opsinjs
HandbookPackages

@opsinjs/preset

The Tailwind theme layer as an importable preset, so many applications can share one token vocabulary without copying a stylesheet into each of them.

Overview

Copying a stylesheet into twelve applications is how twelve applications end up with eleven themes. @opsinjs/preset is the shared, versioned form of the opsinjs theme layer: the @theme inline mappings that turn token custom properties into Tailwind utilities, plus the shape and space scales.

It is build-time configuration, not application code, which is why it is a package rather than something the registry copies. Nobody wants to hand-merge a theme mapping across a fleet.

Nothing is published. The mappings exist in this repository in app/globals.css and app/product.css, and can be copied today.

Where the boundary falls

The first question in the block above is the important one, and the proposed answer is worth stating up front because it determines everything else.

PackageContainsNature
@opsinjs/tailwind.cssThe token values, the degradation media queries, the base layer, the dark variantA stylesheet you @import
@opsinjs/presetThe @theme inline mappings that expose those values as utilities, and the derived scalesConfiguration

Values in one, mappings in the other. The reason for splitting them is that a project may legitimately want opsinjs' utility vocabulary over its own values, as a white-label build with its own ramps would, and that is only possible if the mapping is separable from what it maps.

In practice most projects want both, and importing both is one extra line.

The proposed usage

Tailwind v4 has no JavaScript config, so a "preset" is a CSS file you import:

app/globals.css
/* PROPOSED. Not implemented. */
@import "tailwindcss";
@import "@opsinjs/tailwind.css";
@import "@opsinjs/preset";

Order matters, and it matters more here than in most stacks. app/globals.css in this repository documents its own required order in a header comment, and the positions of the generated token layer and the pinned dark variant are deliberate rather than incidental. See Tailwind v4. The failure mode is silent, which is what makes it worth reading before you assemble your own stylesheet.

What it maps

The utility vocabulary, which is the actual product of this package:

/* Roles are the shadcn-compatible layer */
--color-background, --color-foreground, --color-card, --color-muted,
--color-border, --color-input, --color-ring, --color-primary

/* Status has four ordinal levels, three slots each */
--color-status-steady, --color-status-steady-surface, --color-status-steady-ink
/* … watch, attention, urgent */

/* Category has six ramps */
--color-category-heart, --color-category-mind, --color-category-labs,
--color-category-activity, --color-category-sleep, --color-category-nutrition

/* Motion, shape, space */
--ease-opsin-*, --animate-duration-*, --radius-opsin-*

Every mapping is inline. That is not a style preference: @theme inline resolves the custom property at use time, which is what allows dark mode, the Display-P3 escalation, prefers-reduced-motion, prefers-reduced-transparency and the preview host's data-density attribute to keep reaching a utility. Plain @theme snapshots the value at build time and silently breaks all five.

What it deliberately will not do

It will not add a mix-status-with-category utility, or any convenience that makes it easy to put both colour axes on one element. The whole value of the two-axis model is that violating it takes deliberate effort. See The two colour axes.

It will not ship a text-danger or bg-warning alias for a status colour either. Those names invite a developer to pick a colour because it reads well, which is precisely the decision the status vocabulary exists to take away.

Verify it worked

A utility follows the theme

Apply bg-card, toggle the dark class, confirm it changes. If it does not, something was mapped with @theme rather than @theme inline.

The status utilities exist and are complete

All four levels, all three slots. A partial ramp produces a component that renders correctly in three states and wrongly in the fourth. The fourth is usually the one nobody tested.

Adding a category does not require a fork

Add a seventh category's mappings in your own file, after the preset, and confirm the utility exists. If you have to edit the preset, the third question in the stub block has been answered badly.

Classes written outside your app still survive

A monorepo needs @source lines for packages outside the app directory. Verify in a production build, not in development. See Monorepo.

Troubleshooting

npm install @opsinjs/preset fails. Nothing is published yet.

Utilities exist but produce no colour. The mappings imported without the values. Import @opsinjs/tailwind.css too.

Everything works until a production build. A missing @source. Tailwind scans only what it is told about, and development scans more loosely.

Class sorting stopped working. Under Tailwind v4, prettier-plugin-tailwindcss needs tailwindStylesheet in .prettierrc pointing at your stylesheet, or it silently does nothing.

Next

On this page