---
title: "Astro"
description: "React islands, per-island hydration, and why a health interface should think twice before shipping one."
url: "https://opsinjs.pensievelabs.org/start/installation/astro"
source: "https://opsinjs.pensievelabs.org/start/installation/astro.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["astro install", "astro islands opsinjs"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#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.

<Callout title="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](../troubleshooting.mdx#nothing-is-published-yet). 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.
</Callout>

<Todo>
  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`.
</Todo>

## Set it up [#set-it-up]

<Steps>
  ### Add the React and Tailwind v4 integrations [#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 [#point-source-at-your-component-directory]

  ```css title="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 [#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.
</Steps>

## Understand the hydration caveats [#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](../../accessibility/screen-readers.mdx).

## Verify it worked [#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 [#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](../../project/community.mdx).

## Next [#next]

* [Framework support](../framework-support.mdx) is where the client boundary
  falls in every framework.
* [Vite](./vite.mdx) covers the shared Vite and Tailwind mechanics.
* [Next.js](./next.mdx) has the CSS order rule stated in full.
