---
title: "Vite"
description: "The client-only install, and the source-scanning gotcha that bites hardest here."
url: "https://opsinjs.pensievelabs.org/start/installation/vite"
source: "https://opsinjs.pensievelabs.org/start/installation/vite.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["vite install", "vite react opsinjs"]
---

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

Vite is the simplest opsinjs target because there is no server/client boundary to
reason about: everything is a client tree, so every component is available
everywhere and no import can be in the wrong place. What you lose is server
rendering, which for a health interface is a real cost. First paint is when
someone sees their number.

<Callout title="Built, but no published host yet">
  Every catalogue row is implemented and installable, and each registry item
  carries the source `shadcn add`
  copies. None has been reviewed. What is missing is the host. `opsinjs.pensievelabs.org` is not serving
  yet, so the commands below resolve only against a registry you point `@opsinjs`
  at yourself, and there is no npm package to fall back to.
  [Troubleshooting](../troubleshooting.mdx#nothing-is-published-yet).
</Callout>

Read [Next.js](./next.mdx) first if you have not: the CSS order rule stated there
is the same rule here, and it is stated in full only once.

## Set it up [#set-it-up]

<Steps>
  ### Start from a React + TypeScript Vite app with Tailwind v4 [#start-from-a-react--typescript-vite-app-with-tailwind-v4]

  Tailwind v4 in Vite is installed as a Vite plugin rather than a PostCSS step.
  Confirm Tailwind is generating classes before you add opsinjs. Debugging both at
  once is unpleasant and unnecessary.

  ### Make sure `@/` resolves [#make-sure--resolves]

  The copied components use the `@/` alias. Vite does not provide one, so you need it
  in both places or imports will fail in one of them: `resolve.alias` in
  `vite.config.ts` and `paths` in `tsconfig.json`. They must agree.

  ### Add the registry entry and a component [#add-the-registry-entry-and-a-component]

  <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>
</Steps>

## Get the CSS right [#get-the-css-right]

```css title="src/index.css"
@import "tailwindcss";
@import "./opsinjs.css";

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

Then import that stylesheet exactly once, at your entry point, before your
application mounts.

## Mind the source-scanning gotcha [#mind-the-source-scanning-gotcha]

This is where Vite users lose the most time, and the reason is structural rather
than accidental.

In a framework with a convention-based directory layout, the default `@source`
coverage usually happens to include wherever the CLI put your components. A Vite
app has no such convention: people put components in `src/components`, `src/ui`,
`src/lib/ui`, or wherever the project grew. If the directory you copied into is not
covered, Tailwind generates none of the component's classes and you get a
completely unstyled component with no error message anywhere.

Two habits that prevent it:

* **Declare the directory explicitly** rather than relying on a broad glob you
  believe covers it. An explicit line is self-documenting when someone moves the
  folder.
* **Test the assumption directly.** Put an arbitrary utility class on a copied
  component and see whether it takes effect. That distinguishes "Tailwind is not
  scanning this file" from every other cause in one step.

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

1. **A copied component renders styled.** If not, it is the `@source` line.
2. **Colours resolve through custom properties**, not literals.
3. **`@/` imports resolve at build time and in your editor.** If one works and the
   other does not, `vite.config.ts` and `tsconfig.json` disagree.
4. **Adding `dark` to the root element inverts the theme** with no JavaScript.

## Troubleshooting [#troubleshooting]

**Unstyled components.** The `@source` line, in almost every case. See above.

**`Failed to resolve import "@/components/..."`.** The alias exists in one of
`vite.config.ts` and `tsconfig.json` but not the other, or they point at different
directories.

**Styles work in development, break in the production build.** Development and
production differ in what gets scanned and tree-shaken. Check the `@source`
coverage against the built CSS rather than the dev server's.

**You want server rendering.** Use a framework. Vite alone will not give you it,
and retrofitting it later is more work than starting from
[React Router](./react-router.mdx) or [Next.js](./next.mdx) now.

## Next [#next]

* [components.json](./components-json.mdx) covers the aliases that have to agree
  with your Vite config.
* [Monorepo](./monorepo.mdx) applies if the Vite app is one workspace among
  several.
* [Quick start](../quick-start.mdx) covers the first real screen.
