---
title: "Manual installation"
description: "No CLI. You place the tokens, the utility, and one component by hand, so you can see exactly what lands."
url: "https://opsinjs.pensievelabs.org/start/installation/manual"
source: "https://opsinjs.pensievelabs.org/start/installation/manual.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["manual install", "install without the cli", "no cli install"]
---

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

There is nothing magic in the CLI. It fetches a JSON registry item, writes the
files it names into the directories your `components.json` aliases point at, and
installs any real dependencies. Doing it by hand is entirely reasonable. It is
the right choice in three situations: your platform forbids fetching code at
build time, you want to review every byte before it enters the repository, or
your directory layout is unusual enough that fighting the aliases costs more
than copying four files.

<Callout title="Shipped, and no published host yet">
  The sixty implemented components are real code, and a registry item that
  is being served carries the whole of the source you would otherwise copy by
  hand. What is missing is the host: `opsinjs.pensievelabs.org` does not resolve, so fetching
  a `/r/<name>.json` from it fails, and there is no npm package to fall back to.
  Point the URLs below at a host that is serving the items and the shape is the
  same. [Troubleshooting](../troubleshooting.mdx#nothing-is-published-yet).
</Callout>

## Put the four pieces in place [#put-the-four-pieces-in-place]

<Steps>
  ### The token layer [#the-token-layer]

  One CSS file defining every opsinjs custom property: the two colour axes, the
  material ladder, the motion springs, and the type, space and shape scales. Save it
  as `app/opsinjs.css` or wherever else your stylesheet lives, and import it after
  Tailwind and before your own overrides.

  ```css title="app/globals.css"
  @import "tailwindcss";
  @import "./opsinjs.css";

  @source "./components/opsinjs/**/*.{ts,tsx}";
  ```

  This file is generated from the token source and should be replaced wholesale
  rather than edited. Extensions go in your own `@theme` block afterwards; see
  [Adding your own tokens](../../theming/adding-your-own-tokens.mdx).

  ### The `cn` utility [#the-cn-utility]

  Copied components import a class-merging helper. If you already have shadcn/ui you
  already have it at `lib/utils.ts` and nothing needs doing. If not, it is the
  standard `clsx` plus `tailwind-merge` composition, and it must live at the path
  your imports expect.

  ### The component source [#the-component-source]

  Fetch the registry item for the component you want and write out each file it
  names. A registry item is a JSON document listing files, their target paths, their
  type, and any real dependencies. Nothing is hidden in it, which is the point of
  publishing the format. [registry-item.json](../../registry/registry-item-json.mdx)
  annotates every field.

  ### The dependencies [#the-dependencies]

  Install whatever the registry item's `dependencies` array names. For most opsinjs
  components this is nothing beyond what you already have. Nothing is installed that
  the component does not genuinely need at runtime.
</Steps>

## Fix the import paths [#fix-the-import-paths]

This is the only part that is fiddly by hand. Copied source uses the alias
conventions from `components.json`, such as `@/components/...` and
`@/lib/utils`. If you are not using those aliases, rewrite the imports as you
paste.

Rewriting imports is preferable to inventing a matching alias you will not use
elsewhere, and if you are doing this more than twice it is a strong sign that
adopting the alias convention would be cheaper than the ongoing edit.

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

1. **The component compiles and its imports resolve.** Check `cn` in particular.
2. **It renders styled.** Unstyled means Tailwind is not scanning the directory you
   pasted into.
3. **Its colours come from custom properties.** Literals mean the token layer is
   missing or in the wrong position.
4. **Adding `dark` to the root element inverts the theme** with no JavaScript.
5. **You have recorded what you copied and when**, in your own commit message,
   because the file will not record it for you. Commit the files `shadcn add`
   writes in their own commit, so a later `--diff` has something to compare
   against.

## Troubleshooting [#troubleshooting]

**Unstyled component.** The `@source` line does not cover where you pasted it.

**`cn is not exported`.** The utility is missing or at a different path from the
import.

**Wrong colours.** Import position: the token layer must come after Tailwind.

**You edited the token layer directly and now an update conflicts.** Expected.
Treat that file as generated: replace it, and keep your changes in your own
`@theme` block afterwards.

**You are doing this for every component.** Reconsider the CLI. Manual placement is
a reasonable answer for one or two components and an expensive habit for twenty.

## Next [#next]

* [registry-item.json](../../registry/registry-item-json.mdx) covers the format
  you are reading by hand.
* [Adding your own tokens](../../theming/adding-your-own-tokens.mdx) covers
  extending the token layer without editing it.
* [Upgrading](./upgrading.mdx) shows how to take a change to code you own.
