opsinjs
IntroductionStart hereInstallation

Astro

React islands, per-island hydration, and why a health interface should think twice before shipping one.

Overview

Astro can render opsinjs components as React islands, and for the display-only majority of the roster that works well: they are static markup, so they can be rendered at build time with no hydration at all.

No published host, and this path is not tested

Two separate gaps. opsinjs.pensievelabs.org is not serving yet, so @opsinjs resolves only against a registry you point it at yourself. See Troubleshooting. And Astro has not been exercised against opsinjs at all: the caveats below follow from Astro's island model rather than from a build we have run, which is the more important of the two warnings.

Not written yet.

A verified install, the exact Tailwind v4 plus React integration versions, and worked examples of the hydration directives for each interactive component. This page will be rewritten from a real build before it leaves planned.

Set it up

Add the React and Tailwind v4 integrations

Astro's React integration is what lets a .tsx component render at all; Tailwind v4 is installed as a Vite plugin, since Astro builds on Vite.

Point @source at your component directory

src/styles/global.css
@import "tailwindcss";
@import "./opsinjs.css";

@source "../components/opsinjs/**/*.{ts,tsx}";

Import that stylesheet from your layout so it is present on every page.

Add a component and render it as an island

Display-only components need no hydration directive at all. Add one only when the component genuinely requires client behaviour.

Understand the hydration caveats

This is the part worth thinking about before you commit, because the island model interacts with health interfaces in a specific way.

Display components should not be hydrated. ResultCard, RangeBar, MetricTile, StatusPill, Value and the rest of the display roster are static markup. Rendered without a hydration directive they ship no JavaScript, which is the best possible outcome for a page whose job is to show someone a number quickly on a phone.

Interactive components are whole islands, and islands do not share state. A ConsentSheet and the button that opens it must live inside the same island, or they cannot communicate. Splitting them across two islands is the characteristic Astro mistake, and it fails at runtime rather than at build time.

Theme is a class, so it crosses islands for free. Because opsinjs theming is CSS custom properties rather than React context, every island picks up the theme from the document without a provider inside each one. This is one place where the island model and opsinjs's design agree completely.

Announcements need a live region that outlives the island. If a status changes in response to an interaction, the element that announces it must be in the page, not inside an island that unmounts. See Screen readers.

Verify it worked

  1. A display component renders with no client JavaScript. Check the network tab: if a hydration bundle loads for a static card, a directive is on something that does not need one.
  2. A copied component is styled. Otherwise, @source.
  3. Theme inverts across every island at once when the root class changes.
  4. An interactive component and its trigger are in one island.

Troubleshooting

A control does nothing. It is in a different island from the state it needs. Merge them.

Styles are missing on one page. The layout that imports the stylesheet is not the one that page uses.

Everything hydrates. A hydration directive has been applied more broadly than intended. Often it is on a wrapper rather than on the one component that needs it.

Something framework-specific goes wrong. This path is not tested; please report it. Community.

Next

  • Framework support is where the client boundary falls in every framework.
  • Vite covers the shared Vite and Tailwind mechanics.
  • Next.js has the CSS order rule stated in full.

On this page