Next.js
The reference install. App Router, Tailwind v4, the exact CSS import position, and where the theme class belongs.
Overview
Next.js with the App Router is the reference target: it is what opsinjs is developed against, and the combination most likely to be exercised before a release. Read this page even if you use another framework. The CSS order rule and the theme-class rule are identical everywhere, and this is where they are stated in full.
Shipped, and no published host yet
status-pill and the fifty-nine other implemented components are real code,
and their registry items carry the source shadcn add copies. What does not
exist yet is the canonical host: opsinjs.pensievelabs.org is not serving, so the commands
below resolve only against a registry you point @opsinjs at yourself, and
there is no npm package to fall back to. See
Troubleshooting.
This page assumes a Next.js app on React 19 with Tailwind v4 already working. If Tailwind is not set up, do that first, because opsinjs cannot compensate for a Tailwind installation that is not scanning your source.
Set up the project
Create or open a Next.js app
Nothing about the app has to be special. The App Router is assumed below; the Pages Router works too, with one difference noted at the end.
Add the registry namespace
opsinjs is consumed through the shadcn CLI. If you have never run it in this
project, initialise it first, then add the @opsinjs entry to components.json.
Every field is annotated in components.json.
Add a component
npx shadcn@latest add @opsinjs/status-pillThis copies source into your repository: the component, plus the two lib
modules every opsinjs component imports. It writes no CSS at all. The token
layer is a separate registry item, and until you add it every custom property
these components read resolves to nothing. That failure is what the next
section is about. Presets has the item and what it
writes.
Get the CSS order right
This is the part that is worth reading slowly. opsinjs's stylesheet position is load-bearing, and the failure mode is quiet: components lay out perfectly and are the wrong colour.
@import "tailwindcss";
/* The opsinjs token layer, in the position that makes it work. Adding a
component does not create this: you either vendor the token stylesheet here
or let the theme registry item write its block into this file instead. */
@import "./opsinjs.css";
/* Tailwind v4 finds classes by scanning. The copied components live in your
repository, so the directory holding them must be declared. */
@source "../components/opsinjs/**/*.{ts,tsx}";
/* Your overrides last, so they win. */
@theme {
/* your brand extensions */
}Four rules, in priority order:
- Tailwind first. Everything else layers on top of it.
- The opsinjs token layer second. It defines the custom properties every component reads. If it is imported before Tailwind, Tailwind's own preflight can overwrite parts of it.
@sourcefor every directory holding copied components. Without this, Tailwind never sees the classes and generates none of them.- Your overrides last. Anything you want to win goes after the token layer, never inside it. Editing the token layer directly makes the next upgrade a manual merge for no benefit.
Tailwind v4 covers @theme versus @theme inline
and the other ordering hazards.
Wire the theme mode
There is no opsinjs provider. Light and dark are a class on the root element, and the tokens resolve from there:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
)
}Two details that matter more than they look:
langis not optional. Screen readers select a pronunciation dictionary from it, and a health interface reading numbers and units aloud in the wrong language is a real accessibility failure, not a cosmetic one.suppressHydrationWarningon<html>is what lets a theme script set the class before React hydrates without producing a mismatch warning. It suppresses the warning on that element only, not on your tree.
If you want a system-preference toggle, any theme library that sets a class on the root element will do; opsinjs takes no opinion and adds no dependency.
Render something
StatusPill is implemented, and this is the whole of its simplest use. The
level is a required prop with no default, and describes is what a screen
reader announces the level as being about. Without it a listener hears a
verdict with no subject:
import { StatusPill } from "@/components/opsinjs/status-pill"
export default function Page() {
return <StatusPill status="watch" describes="Blood pressure" />
}StatusPill is display-only, so this stays a server component and ships no
JavaScript. Components that need a client boundary say so on their own pages;
Framework support
explains which and why.
Verify it worked
These are the checks that decide whether the install is correct, in the order that isolates the most failures soonest.
- The page renders and the pill is styled. Unstyled means the
@sourceline is missing or points at the wrong directory. - Inspect the pill's background. It should resolve through a custom property in the opsinjs status scope. A literal colour means the token layer is missing or imported in the wrong position.
- Add
class="dark"to<html>by hand. Colours should invert with no JavaScript involved at all. If they do not, the token layer is not loading its dark block. - Check the network tab. A page containing only display components should ship no additional client JavaScript for them.
- Switch your display to greyscale. The pill must still say which status it is, in words. If it does not, the component is being used wrongly rather than installed wrongly.
Troubleshooting
Unstyled components. The @source path is relative to the CSS file, and it is
easy to be one directory out. Confirm by putting a deliberate arbitrary class on a
copied component and checking whether it takes effect.
Colours are wrong or fall back to defaults. Import position. Move
@import "./opsinjs.css" directly after @import "tailwindcss".
A hydration warning about the theme class. suppressHydrationWarning is
missing from <html>.
useState errors in a server component. You imported an interactive
component into a server tree. Mark the leaf as a client component, not the page.
Pages Router. Everything above applies except that there are no server
components, so the client-boundary section is moot. Import the stylesheet in
pages/_app.tsx and set the theme class in pages/_document.tsx.
Turbopack versus webpack. opsinjs adds no bundler configuration and needs none. If a bundler-specific problem appears, it is a Tailwind or Next question before it is an opsinjs one.
Next
- components.json covers every field opsinjs reads, annotated.
- Quick start covers the first real screen.
- Tailwind v4 has the ordering rules in full.