---
title: "@opsinjs/preset"
description: "The Tailwind theme layer as an importable preset, so many applications can share one token vocabulary without copying a stylesheet into each of them."
url: "https://opsinjs.pensievelabs.org/packages/opsinjs-preset"
source: "https://opsinjs.pensievelabs.org/packages/opsinjs-preset.md"
section: "Other pages"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["tailwind preset", "shared theme", "monorepo theme", "theme package"]
---

> 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.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<PageTemplate kind="guide" />

## Overview [#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.

<StubNotice
  name="opsinjs-preset"
  issue="prashantonomy/opsinjs#0"
  questions="[
  &#x22;In Tailwind v4, where does the boundary fall between this package and @opsinjs/tailwind.css?&#x22;,
  &#x22;Should the preset carry values, or only the @theme inline mappings that reference them?&#x22;,
  &#x22;How does a consumer add a seventh category without forking the preset?&#x22;,
]"
/>

**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 [#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`](./opsinjs-tailwind-css.mdx) | 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 [#the-proposed-usage]

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

```css title="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](../theming/tailwind-v4.mdx). The failure mode is silent, which is
what makes it worth reading before you assemble your own stylesheet.

## What it maps [#what-it-maps]

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

```css
/* 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 [#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](../health/two-colour-axes.mdx).

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 [#verify-it-worked]

<Steps>
  ### A utility follows the theme [#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 [#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 [#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 [#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](../start/installation/monorepo.mdx).
</Steps>

## Troubleshooting [#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`](./opsinjs-tailwind-css.mdx) 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 [#next]

* [`@opsinjs/tailwind.css`](./opsinjs-tailwind-css.mdx) is the values half.
* [Tailwind v4](../theming/tailwind-v4.mdx) covers `@theme inline`, `@source`,
  and the ordering that breaks quietly.
* [Adding your own tokens](../theming/adding-your-own-tokens.mdx) covers
  extending the vocabulary without forking it.
