opsinjs
IntroductionStart hereInstallation

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

This 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.

app/globals.css
@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:

  1. Tailwind first. Everything else layers on top of it.
  2. 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.
  3. @source for every directory holding copied components. Without this, Tailwind never sees the classes and generates none of them.
  4. 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:

app/layout.tsx
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:

  • lang is 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.
  • suppressHydrationWarning on <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:

app/page.tsx
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.

  1. The page renders and the pill is styled. Unstyled means the @source line is missing or points at the wrong directory.
  2. 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.
  3. 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.
  4. Check the network tab. A page containing only display components should ship no additional client JavaScript for them.
  5. 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

On this page