---
title: "Framework support"
description: "Which React setups opsinjs targets, what it requires of them, and where the server/client boundary falls."
url: "https://opsinjs.pensievelabs.org/start/framework-support"
source: "https://opsinjs.pensievelabs.org/start/framework-support.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["framework support", "supported frameworks", "rsc support", "server components"]
---

> 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]

opsinjs is React-only, and there is no plan for a Vue, Svelte or web-component
build. The tokens are plain CSS custom properties and are reusable anywhere; the
components are not, and pretending otherwise would produce a worse React library.

Within React the question is not really "which framework" but "which four
capabilities does your setup have". Those four are what this page is about.

<Callout title="The tested matrix is not published yet">
  Every catalogue row is implemented and installable, and none of the rows below
  has a recorded test result: no framework in the matrix
  has been exercised end to end and the result written down. The matrix fills from
  generated results once there are any. What is stated here instead is the
  *requirement set* and the boundary rules, which are design decisions and are true
  now.
</Callout>

## Meet four requirements [#meet-four-requirements]

Everything else follows from these. If your setup has all four, opsinjs is
intended to work; if it is missing one, the gap is usually specific and fixable.

<Steps>
  ### React 19 or later [#react-19-or-later]

  Not negotiable. opsinjs is built on Base UI, which targets modern React, and
  several components depend on React 19 form and ref behaviour. React 18 is not
  supported and will not be back-ported.

  ### Tailwind CSS v4 [#tailwind-css-v4]

  Also not negotiable. The token layer is authored as CSS custom properties consumed
  through Tailwind v4's `@theme` mechanism, and there is no `tailwind.config.js` to
  port. Tailwind v3 users need to upgrade first;
  [Tailwind v4](../theming/tailwind-v4.mdx) covers the parts of that upgrade that
  interact with opsinjs.

  ### A bundler that can scan your source for classes [#a-bundler-that-can-scan-your-source-for-classes]

  Tailwind v4 discovers classes by scanning declared sources. Because opsinjs
  components are *copied into your repository* rather than installed as a package,
  the directory you copy them into must be scanned. This is the single most common
  setup failure and it produces a component that renders with no styling at all. See
  [Monorepo](./installation/monorepo.mdx) for the `@source` lines.

  ### A place to import one stylesheet, in a known position [#a-place-to-import-one-stylesheet-in-a-known-position]

  Order is load-bearing. The opsinjs token layer must come after Tailwind and after
  any base-layer reset, and before your own overrides. Each installation page states
  the exact position for that framework.
</Steps>

## Know where the client boundary falls [#know-where-the-client-boundary-falls]

This is the part that differs most between frameworks, and the part where an
incorrect assumption costs a day.

**Many opsinjs components are server-renderable.** Twenty-seven of the sixty carry
no `"use client"` directive at all. A `ResultCard` showing a value, a range and a
status is static markup; there is no reason for it to ship JavaScript, and it does
not.

**The rest require a client boundary**, and each one says so on its own page in
its cost section. Thirty-three of the sixty carry `"use client"` today, among them
`Sheet`, `Dialog`, `ConsentSheet`, `LogSheet` and `ReadingInput`, which manage
focus and open/closed state; `Term`, whose plain-English expansion is a disclosure
the reader opens; and `RelativeTime`, for the reason below. `TrendSparkline` is not
among them. It renders as static markup, so a series is safe in a server tree.
Where a component is split, the interactive part is a separate file, so importing
the display half does not drag a client boundary into your server tree.

**Theme and density are CSS, not context.** There is no provider to mount for
theming: the mode is a class and the tokens are custom properties. That is what
makes server rendering practical, and it is why a flash of incorrect theme is a
solved problem rather than a known issue.

`RelativeTime` is the honourable exception worth knowing about in advance:
"3 hours ago" cannot be computed identically on a server and a client without care,
and its page documents the hydration-safe contract rather than leaving you to
discover the mismatch.

## Frameworks in scope [#frameworks-in-scope]

| Setup                       | Intended status        | What is specific about it                                                              |
| --------------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Next.js App Router          | Primary target         | The reference install; server components by default. [Guide](./installation/next.mdx)  |
| Next.js Pages Router        | Supported, not primary | No server components; everything is a client tree. Global CSS position still matters.  |
| Vite + React                | Supported              | Client-only. The `@source` gotcha bites hardest here. [Guide](./installation/vite.mdx) |
| React Router framework mode | Supported              | Server rendering with a different data story. [Guide](./installation/react-router.mdx) |
| TanStack Start              | Intended               | [Guide](./installation/tanstack-start.mdx)                                             |
| Astro with React islands    | Intended, with caveats | Per-island hydration changes the boundary calculus. [Guide](./installation/astro.mdx)  |
| Remix (pre-React-Router-7)  | Not targeted           | Use the React Router framework-mode path after upgrading.                              |
| Create React App            | Not targeted           | Unmaintained; no Tailwind v4 story worth documenting.                                  |
| React Native                | Not targeted           | The tokens are portable; nothing else is.                                              |

<NoDataYet script="scripts/build-reference.mts" />

## Verify it worked [#verify-it-worked]

Four checks that between them catch almost every framework-level setup problem:

1. **A copied component renders styled.** If it renders unstyled, Tailwind is not
   scanning the directory you copied it into. This is not an opsinjs problem and
   no opsinjs setting fixes it.
2. **A token resolves.** Inspect any opsinjs element and confirm its colour comes
   from a custom property rather than a literal. If it is literal, the token layer
   is missing or imported in the wrong position.
3. **The server tree stays a server tree.** Import a display-only component into a
   server component and confirm your framework does not complain. If it does, you
   have imported the interactive half.
4. **Dark mode does not flash.** Toggle the theme class and confirm nothing
   re-renders. Theming is CSS; if something re-renders, a provider has crept in.

## Troubleshooting [#troubleshooting]

**`useState` / `createContext` errors in a server component.** You imported an
interactive component into a server tree. Split the import, or mark the leaf as a
client component rather than the whole page. Marking the whole page is the
reflex fix and the expensive one.

**Styles work in development and vanish in production.** Almost always a Tailwind
source-scanning difference between the two builds. Check your `@source` lines
against [Monorepo](./installation/monorepo.mdx).

**Hydration mismatch on a time or a formatted number.** Time zone or locale
differs between server and client. `RelativeTime` and `Value` document the
hydration-safe contract; this is the one class of mismatch the components take
responsibility for.

**Tailwind v3.** Upgrade first. There is no v3 compatibility layer and there will
not be one.

**Your framework is not listed.** If it runs React 19 and Tailwind v4 and lets you
control stylesheet order, it will most likely work. Tell us and we will add it to
the matrix once there is something to test.

## Next [#next]

* [Installation](./installation/index.mdx) is where you pick the path that
  matches your setup.
* [Browser support](./browser-support.mdx) is the runtime half of the support
  question.
* [Tailwind v4](../theming/tailwind-v4.mdx) covers `@theme` versus
  `@theme inline`, and the CSS order that breaks silently.
