---
title: "Presets"
description: "How a whole opsinjs theme is packaged as one installable artifact, why opsinjs preset names are namespaced, and which presets exist today."
url: "https://opsinjs.pensievelabs.org/theming/presets"
source: "https://opsinjs.pensievelabs.org/theming/presets.md"
section: "Theming & tokens"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["preset", "theme preset", "opsinjs-calm", "shareable theme"]
---

> 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.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#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 [#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](../registry/namespaces.mdx). 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 [#what-is-inside-a-preset]

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

| Included                                                                                                                                    | Not included                     |
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `--background`, `--foreground`, `--card`, `--muted`, `--primary`, `--border`, `--ring` and their `-foreground` partners, for light and dark | The category ramps               |
| `--radius` and the derived radius scale                                                                                                     | The status ramps                 |
| `--spacing`                                                                                                                                 | The material ladder              |
| `--font-sans` and `--font-mono` declarations                                                                                                | The motion springs               |
| An `@supports (color-gamut: p3)` escalation for the roles it defines                                                                        | Any 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 [#the-presets-that-exist-today]

<Todo>
  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.
</Todo>

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.

<NoDataYet script="scripts/build-registry.mts" />

## Installing a preset [#installing-a-preset]

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

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add @opsinjs/opsinjs-default
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add @opsinjs/opsinjs-default
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add @opsinjs/opsinjs-default
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add @opsinjs/opsinjs-default
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add https://opsinjs.pensievelabs.org/r/themes/opsinjs-default.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add https://opsinjs.pensievelabs.org/r/themes/opsinjs-default.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add https://opsinjs.pensievelabs.org/r/themes/opsinjs-default.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add https://opsinjs.pensievelabs.org/r/themes/opsinjs-default.json
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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 [#publishing-a-preset-of-your-own]

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

<Steps>
  ### Generate the theme [#generate-the-theme]

  Use [the theme generator](./theme-generator.mdx) and take the preset export
  rather than the raw CSS.

  ### Wrap it as a registry item [#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](../registry/registry-item-json.mdx).

  ### Choose a name that cannot collide [#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 [#serve-it-and-validate-it]

  Publish the JSON, then run the checks in
  [Validating your theme](./validating-your-theme.mdx) 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.
</Steps>

## Verify it worked [#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 [#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](../registry/namespaces.mdx) 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 [#next]

* [Validating your theme](./validating-your-theme.mdx) comes before you publish.
* [Preset codes](../registry/preset-codes.mdx) is the short-code form, and
  shows how encode, decode and apply work.
* [Design handoff](./design-handoff.mdx) covers getting the same theme into a
  design tool without retyping it.
