@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.
| Package | Contains | Nature |
|---|---|---|
@opsinjs/tailwind.css | The token values, the degradation media queries, the base layer, the dark variant | A stylesheet you @import |
@opsinjs/preset | The @theme inline mappings that expose those values as utilities, and the derived scales | Configuration |
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:
/* 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
@opsinjs/tailwind.cssis the values half.- Tailwind v4 covers
@theme inline,@source, and the ordering that breaks quietly. - Adding your own tokens covers extending the vocabulary without forking it.
@opsinjs/react
The components as a conventional npm dependency, for teams that cannot vendor source. The trade is stated plainly before you take it.
@opsinjs/tailwind.css
The opsinjs stylesheet as a versioned import, carrying token values, preference degradations, the base layer and the dark variant, in the one order that works.