opsinjs
FoundationsTheming & tokens

Presets

How a whole opsinjs theme is packaged as one installable artifact, why opsinjs preset names are namespaced, and which presets exist today.

Overview

A preset is an entire theme reduced to one addressable thing: a name you can paste into a CLI command, a URL that returns JSON, and a block of CSS variables it expands into. It exists so that "use our theme" is one line in a README rather than a paragraph of instructions and a screenshot of a stylesheet.

opsinjs presets are ordinary shadcn-spec registry items of the theme kind. There is no opsinjs-specific format to learn, no plugin to install, and no lock-in: anything that can consume a shadcn registry can consume an opsinjs preset, and a project that later drops opsinjs keeps the CSS.

Why the names are namespaced

Every opsinjs preset name begins with opsinjs-.

This is not decoration. shadcn ships style presets of its own: lyra, nova, vega and the rest. This documentation site is itself built on one of them. If opsinjs published a preset called calm and shadcn later published a different calm, a project with both registries configured would resolve one of them and the reader would have no way to tell which. Namespacing the value, not just the registry, makes the collision impossible rather than unlikely.

The same reasoning produces the registry namespace @opsinjs, described in Namespaces. A preset is addressed as @opsinjs/opsinjs-calm when the registry is configured, and by full URL when it is not.

What is inside a preset

A preset carries role tokens and shape, and nothing else.

IncludedNot included
--background, --foreground, --card, --muted, --primary, --border, --ring and their -foreground partners, for light and darkThe category ramps
--radius and the derived radius scaleThe status ramps
--spacingThe material ladder
--font-sans and --font-mono declarationsThe motion springs
An @supports (color-gamut: p3) escalation for the roles it definesAny component's tier-3 variables

The right-hand column is the interesting one. A preset cannot change what "urgent" looks like, cannot add a health category, and cannot make a translucent material opaque, because those are system-wide invariants rather than brand decisions. A theme that could change them would be able to break a screen's meaning by being installed, which is not a property you want in a one-line install.

The presets that exist today

Not written yet.

Exactly one theme is published: the default that tokens/*.json generates. The named alternatives below the fold on the roadmap are a higher-contrast variant and a large-type variant. Both are specified but not built, and are deliberately absent from the registry rather than present and empty.

The default theme needs no preset to use; it is what you get from a plain install. It is listed in the registry as opsinjs-default so that a project which has drifted can get back to it explicitly.

This table has not been generated. It is produced by scripts/build-registry.mts. Run pnpm run generate and reload.

Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.

Installing a preset

Once the @opsinjs registry is configured in your components.json:

npx shadcn@latest add @opsinjs/opsinjs-default

Without the registry configured, the full URL works and requires nothing:

npx shadcn@latest add https://opsinjs.pensievelabs.org/r/themes/opsinjs-default.json

The CLI writes the CSS variables into the stylesheet named by tailwind.css in your components.json. It appends; it does not merge, and it does not remove a previous preset's declarations. Applying a second preset over a first leaves you with both, and the later one wins by cascade order. That is usually what you want, and is occasionally a very confusing bug. Delete the old block.

Publishing a preset of your own

You do not need opsinjs' permission or its registry. A preset is a JSON file at a URL.

Generate the theme

Use the theme generator and take the preset export rather than the raw CSS.

Wrap it as a registry item

Give it a name, a type of registry:item, no files at all, and a cssVars object with light, dark and optionally theme keys. A theme is the one registry item that is pure configuration. The field-by-field annotation is in registry-item.json.

Choose a name that cannot collide

Prefix it with your own organisation, not with opsinjs-. The namespace argument above applies to you exactly as it applies here.

Serve it and validate it

Publish the JSON, then run the checks in Validating your theme against the served file rather than against your local copy. The two differ more often than you would expect, usually because of a build step that strips comments and a CDN that caches the old one.

Verify it worked

Load your app, open devtools, and confirm that --primary on :root holds the preset's value and that toggling dark swaps it. Then confirm the negative: the value of --opsin-status-urgent-line must be unchanged by installing a preset. If it moved, the preset is writing outside tier 2 and should be fixed before anyone else installs it.

Troubleshooting

The CLI says the item was added but nothing changed. It appended the block above an existing one, or tailwind.css in components.json points at a stylesheet your app does not actually import.

Two presets are installed and the wrong one wins. Cascade order, not a bug. Remove the block you do not want.

@opsinjs/… is not found. The registry is not configured. Either add it to components.json as Namespaces describes, or use the full URL form, which never needs configuration.

A preset from another project applies but looks wrong under opsinjs. It was probably built for shadcn's default role set, which is larger than the product theme's. Missing roles fall back rather than fail, which reads as "nearly right" and is harder to debug than an error.

Next

On this page