---
title: "Add your first component"
description: "One command, what it will put in your project, and nothing else to read."
url: "https://opsinjs.pensievelabs.org/start/add-your-first-component"
source: "https://opsinjs.pensievelabs.org/start/add-your-first-component.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["add a component", "one command install", "copy in a component"]
---

> 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]

This is the sixty-second path. If you want the reasoning, the safety rules and the
review checks, [Quick start](./quick-start.mdx) is the longer version. If you want
to know what the command will actually write into your repository before you run
it, that is here too, at the bottom.

<Callout title="Shipped, and no published host yet">
  `status-pill` is implemented and its registry item carries the whole of its
  source, so the command below is a real one rather than a sketch. What is
  missing is somewhere to fetch it from by default: there is no npm package and
  there will not be one, and `opsinjs.pensievelabs.org` is not serving yet, so
  `@opsinjs` resolves only once you point it at a host that is serving.
  [components.json](./installation/components-json.mdx) has the entry. The
  component is `shipped`: the API may change in any release, and it was
  audited against WCAG 2.2 AA by its own authors rather than an independent
  reviewer (see [ADR 0025](../project/decisions/0025-the-audit-is-author-run.mdx)),
  and it has had no clinical review, so use it in a prototype and not in
  front of a patient.
</Callout>

## Run one command [#run-one-command]

<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/status-pill
    ```
  </CodeBlockTab>

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

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

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

That is the whole path. No provider to mount, no plugin to register, no
`tailwind.config` to edit. opsinjs is Tailwind v4 and CSS-variable native, so
the tokens arrive as CSS.

## Know what it writes [#know-what-it-writes]

Three files, so it is not a surprise:

```text
<aliases.ui>/status-pill.tsx     the component source is yours now, edit freely
<aliases.lib>/opsinjs.ts         units, categories, error codes: what every component reads
<aliases.lib>/status.ts          the four clinical levels, their words and their glyphs
```

The item declares those destinations as `@ui/…` and `@lib/…`, so where they
actually land is whatever your `components.json` aliases say. In a default
shadcn project, that is `components/ui/status-pill.tsx` and `lib/`. Two things
the command does **not** write, and both are commonly assumed. It writes no CSS:
the token layer is a separate registry item and
[Presets](../theming/presets.mdx) has it. And it does not add the `@opsinjs`
entry to `components.json`. That entry is the precondition for the command
resolving at all, so you add it first, by hand.

The only entry added to `package.json` is `lucide-react`, which is where the
level glyphs come from and which any project with `iconLibrary` set to lucide
already has. That is the icon library opsinjs assumes. opsinjs distributes
**source you own**, not a package you upgrade. That is a deliberate trade: you
get to change anything, and in exchange you take on the merge when we change it.
[Upgrading](./installation/upgrading.mdx) is how that merge is made survivable.

## Verify it worked [#verify-it-worked]

Three checks, in order of how much they tell you:

1. **The file is there and it compiles.** It landed where `aliases.ui` points,
   it imports cleanly, and your typechecker is quiet. If the only complaint is
   `@/lib/utils`, `shadcn init` was never run in this project: the registry does
   not ship that file and every copied component imports `cn` from it.
2. **The tokens resolved.** Render one pill and inspect it: its background should
   come from a custom property in the opsinjs status scope, not from a literal
   colour. If you see a hard-coded hex, the token layer was not imported.
3. **It survives greyscale.** Switch your display or your devtools to greyscale.
   The pill must still say which level it is, in words and by its icon. If it does
   not, the token layer loaded but the component is being used wrongly. Colour
   is never the only carrier of clinical status.

<Todo>
  Nobody has walked this path end to end against a published registry host. Every
  step below was verified against a local registry server, so what is unverified is
  the one thing a reader cannot substitute for themselves: that `@opsinjs` resolves,
  that the item fetches over the network, and that the file `shadcn add` writes is
  byte for byte the file this repository serves.
</Todo>

## Troubleshooting [#troubleshooting]

**`Unknown registry @opsinjs`.** `components.json` has no `registries` entry
mapping the namespace to a URL. Nothing preconfigures it and the CLI has no
default for it, so this is the first thing to check; see
[components.json](./installation/components-json.mdx). A DNS or network failure
rather than an unknown-registry error means the entry is there and points at
`opsinjs.pensievelabs.org`, which is not serving yet.

**The component renders unstyled.** The token layer is not imported, or it is
imported before Tailwind. CSS order in opsinjs is load-bearing and the exact
position is specified in [Next.js](./installation/next.mdx) and
[Tailwind v4](../theming/tailwind-v4.mdx).

**Tailwind classes inside the copied file do nothing.** Tailwind v4 scans sources
declared with `@source`. A file copied into a directory Tailwind is not watching
produces exactly this symptom. See
[Monorepo](./installation/monorepo.mdx) for the `@source` lines.

## Next [#next]

* [Quick start](./quick-start.mdx) has the same command with the reasoning
  attached.
* [StatusPill](../components/status-pill.mdx) is the specification, including
  the three things it must never be used for.
* [Clinical status semantics](../health/clinical-status-semantics.mdx) says what
  the four levels mean before you assign one.
