---
name: usva
description: >
  Use when building, restyling, or reviewing any UI in a project that has
  usva installed or has copied components from https://usva.build/r.
  Covers choosing a component, composing layers, theming by role token, and
  the silent failure modes specific to this library. Also use when the user
  mentions usva, kajo, sisu, savi, sula, or an atmosphere by name.
compatibility: >
  React 18 or 19. Tailwind v4, since the tokens ship as an @theme preset and
  use v4 utility names. Works in the Next.js App Router; most primitives are
  server components, so the client boundary matters.
license: MIT with Commons Clause
metadata:
  author: matt-pasek
  version: "1"
---

# usva.

a React design system with a design language attached. this file is the language: the
rules about what to reach for, what to leave alone, and the handful of ways this library
fails silently if you guess.

## golden rules

read these first. if you read nothing else in this file, these are the seven
that make the difference between using usva and merely importing from it.

1. **import from the subpath, never the barrel.** `usva/primitives/button`,
   not `usva`. the barrel's export graph is the entire library: every
   atmosphere, all of sula, all of motion. how much of that your bundler prunes is its
   business, and not something worth betting a page's weight on. the subpath makes the
   question moot.
2. **intensity is a property of the layer, not of your taste.** you do not get to decide
   that a component should be louder here.
3. **one sula element per region, at most.** two in one region fight and read as noise.
4. **never a raw hex.** every colour is a role token.
5. **`faint` is decorative.** it must never be the only thing carrying meaning.
6. **never wrap a live atmosphere or sula canvas in an element that animates**
   **`transform` or `clip-path`.** chrome blanks the canvas and nothing errors.
7. **retheme by moving role tokens.** never fork a component to change a colour.

## what usva is

an authored React design system: beauty that stays usable. it ships two ways from one
source, and you pick per component. 79 components, 3 themes.

| how | command | you get |
| --- | --- | --- |
| registry | `npx shadcn add https://usva.build/r/button.json` | the source, copied in, yours to fork |
| package | not on npm yet | `usva-tokens` is published; `usva` follows |

use the registry. `bun add usva` will fail today, so do not offer it.

## the fastest correct path

```bash
npx shadcn add https://usva.build/r/button.json
bun add usva-tokens
```

```css
/* globals.css */
@import "tailwindcss";
/* anything shadcn init wrote goes here, above the usva imports */
@import "usva-tokens/theme.css";
@import "usva-tokens/themes/kajo.css";
```

```tsx
import { Button } from "@/components/ui/button";

export function Save() {
  return <Button variant="solid">save</Button>;
}
```

that is a correct usva install. everything below is about not getting the next part wrong.

## the one rule

intensity is a property of the layer. five layers, five intensities, and the layer you
reach into decides how loud the result is allowed to be.

| layer | intensity | import root |
| --- | --- | --- |
| core · primitives | recedes | `usva/primitives/<slug>` |
| core · patterns | structures | `usva/patterns/<slug>` |
| core · motion | guides | `usva/motion/<slug>` |
| sula | asserts | `usva/sula/<slug>` |
| atmospheres | room | `usva/atmospheres/<slug>` |

core recedes so the content is the thing you see. patterns structure it. motion guides
the eye. sula asserts, loudly and on purpose. an atmosphere is not a component you place,
it is the room the rest of it stands in.

**a region** is a bounded area competing for a single attention focus. this is the unit the
sula rule counts. a `SulaFrame` at the viewport edge is ambient and at the boundary; a
`SulaNav` at the top is a focal control surface. they are different regions, they do not
compete, and they ship together. two fluid fields inside one card are one region and they
fight.

## the workflow

1. **name the region and what it is for.** a hero, a settings form, a table someone works
   in all day. this answers the intensity question before you pick anything.
2. **pick the layer that region deserves, then the component inside it.** the index at the
   bottom of this file is grouped by layer for this step.
3. **import from the subpath.** golden rule 1.
4. **colour it with role tokens.** never a hex, never a raw tailwind palette colour.
5. **verify** against the never-list and the gotchas table. most usva mistakes are silent,
   so a green build and a screenshot that looks fine are not evidence.

## never

- two sula elements in one region. `SulaFrame` plus `SulaNav` is legal because those are
  two regions; two fields in one card is not.
- a sula element inside dense, task-bound UI. not a table, not a form, not a dashboard
  someone is working in.
- a raw hex, or a tailwind palette colour like `bg-slate-800`. role tokens only.
- `faint` as the only thing carrying meaning. labels, provenance, IPA, anything readable
  is `muted` at minimum, which clears 4.5:1.
- forking a component to change a colour. move the token.
- the barrel import.
- `vw` units for anything that might get nested. see the gotchas.
- an atmosphere used as decoration on a busy page. it is the room, not an object in it.

## themes and tokens

24 semantic roles, and no component reaches outside them:

```
bg · sunken · surface · surface-2 · overlay · scrim · ink · muted · faint · on-accent · on-sunken · on-tint · accent · accent-2 · accent-alt · accent-tint · accent-ink · success · warning · danger · info · border · border-strong · ring
```

`ink` and `muted` clear 4.5:1 on every surface role and carry information. `faint` is
decorative and never load-bearing. as tailwind utilities these are `bg-surface`,
`text-ink`, `border-border` and so on, and as raw variables `var(--usva-surface)`.

3 themes: kajo, sisu, savi. kajo is declared at `:root` as well as
`[data-theme="kajo"]`, so it is what you get by default.

**subtree theming works.** `theme.css` carries a `[data-theme]` block that remaps every
`--color-*` role from its `--usva-*` source, so this genuinely rethemes the subtree and
`bg-surface` inside it resolves to sisu's surface:

```tsx
<div data-theme="sisu">
  <Card>…</Card>
</div>
```

worth stating because it is the kind of thing that is broken in most token systems.

**4 atmospheres are dark-only:** kuulto, hehku, loimu, kajastus. on a light theme they
render wrong. they export `hiddenOnGround` from `atmospheres-core` for exactly this case,
so use that or pick a different atmosphere.

`prefers-reduced-motion` is handled for you: the GL canvases check it centrally and the
transition utilities in `theme.css` self-guard. you do not need to add a fallback, and you
should not remove theirs.

## gotchas

every row here is a bug that already happened in this codebase. they share one property:
**they fail quietly.** the build passes, the types pass, and the screenshot often looks
fine. none of them is guessable from a component list, which is why this file exists.

| trap | why it bites | what to do instead |
| --- | --- | --- |
| the barrel import | the root export's graph is the whole library, so what a page actually ships depends on how well your bundler prunes it. the failure mode is a static badge that arrives with the shader and animation runtimes | subpath import, always |
| client boundary | a value imported from a `"use client"` module reaches a server component as a client reference, so a comparison against it silently returns `false`, then behaves correctly once hydrated | never compare a server-side value against an export from a client module |
| animated ancestor | chrome blanks a live GL canvas while an ancestor animates `clip-path` or `transform`. no error, just an empty canvas | reveal it with a sibling cover, never a wrapping animation |
| `whileInView` never fires | `Reveal` uses `viewport={{ once: true }}`. a child parked outside its own `overflow-hidden` mask never intersects, so it stays invisible forever | observe the mask, animate the child |
| `rim-light` on a circle | it is a straight bar across the bounding box, so it cuts a visible seam through anything `rounded-full` | `shadow-raised` |
| scaling a `SulaField` | the simulation runs in field units. a css `transform: scale` makes the pool spill outside its bounds | size the container, never scale the component |
| `vw` sizing | reads correctly at full width and breaks the moment the component is nested in a narrower column | `cqi` inside a `@container` |
| dark-only atmospheres | four of them render wrong on a light theme | `hiddenOnGround`, or choose another |
| usva css imported above shadcn's | `shadcn init` appends an `@theme inline` block, and `border`, `accent`, `muted` and `ring` are role names in both. the later block wins, so those four resolve to shadcn's neutrals while the other twenty-one stay ours. the component reads as deliberate; an outline `Button` with a near-white border is the tell | import the usva css last |

## what each choice costs

this is why golden rule 1 is a rule and not a preference. the subpath keeps these scoped
to the components that actually need them:

| package | pulled in by |
| --- | --- |
| `motion` | `Button`, `FeatureCarousel`, all six sula components, `Reveal`, `PageTransition`, `atmospheres-core` |
| `ogl` | `sula-core`, the atmosphere renderers |
| `@base-ui/react` | `Tabs`, `Drawer`, `Tooltip`, `Radio`, `HintPopover`, `Toast` |
| `@dnd-kit/core` | `DashboardGrid` |
| `highlight.js` | `CodeSnippet` |

`Button` importing `motion` is the one worth remembering: even the plainest primitive here
sits on the animation runtime, so "it is only a button" is not a reason to be careless about
how you import it.

`cn` has its own entry, `usva/cn`, so it is not trapped in a client chunk.

## server and client

usva components are **not** uniformly client components, which is unusual and worth
knowing: a little over a third of the primitives carry `"use client"`, and most do not.
so most primitives render on the server, and you should not reflexively push a boundary
down to use one.

all of motion, and every sula and atmosphere component that owns a canvas, is a client
component. in the Next.js App Router you can import any of them from a server component
and the framework handles the boundary. never delete a `"use client"` directive.

## when not to reach for usva

- when the host app already owns the component. usva is not a reason to replace a working
  form field.
- when the region wants a plain surface. `bg-surface` and a border is a legitimate answer,
  and it is usually the right one. an atmosphere is not an upgrade.
- for generic shadcn/ui primitives that usva does not ship. check the index first.
- to add visual interest to a page that is already busy. the fix there is subtraction.

## the index (79)



grouped by layer, because the layer is the decision. the import path is complete as

written: append nothing, and the named export matches the component name.



### core · primitives (36) · recedes

- Announcement · recedes · `usva/primitives/announcement`
  a one-line notice above the fold, for a release or a warning you cannot bury.
- Avatar · recedes · `usva/primitives/avatar`
  a person, as an image or their initials. groups into a cluster with a +N chip.
  - AvatarGroup · ships with Avatar, same import path
- Badge · recedes · `usva/primitives/badge`
  the status of the thing it sits on. it labels, it never clicks.
- Button · recedes · `usva/primitives/button`
  the one thing you want pressed, with a loading, success and error state built in.
- Callout · recedes · `usva/primitives/callout`
  a note that stays put. a toast leaves on its own, this one waits to be read.
- Card · recedes · `usva/primitives/card`
  a surface that groups things. four skins, from elevated to a bare outline.
  - GlowCard · ships with Card, same import path
- Checkbox · recedes · `usva/primitives/checkbox`
  an independent yes or no. use it when the options do not exclude each other.
- Chip · recedes · `usva/primitives/chip`
  a tag. a keyword, a filter, a stack entry. removable when it is a filter.
- CodeSnippet · recedes · `usva/primitives/code-snippet`
  a highlighted code block with the copy button already there.
- ColorField · recedes · `usva/primitives/color-field`
  picks a colour and shows you the value. the swatch is the control, not a decoration.
- Dialog · recedes · `usva/primitives/dialog`
  interrupts, on purpose. focus trapped, scroll locked, escape closes.
- Drawer · recedes · `usva/primitives/drawer`
  a dialog that slides in from an edge. use bottom on mobile, side for a detail panel.
- DropdownMenu · recedes · `usva/primitives/dropdown-menu`
  a list of actions, anchored to what triggered them. actions, not navigation.
- HintPopover · recedes · `usva/primitives/hint-popover`
  the explanation a tooltip cannot hold, because this one has a button in it.
- Input · recedes · `usva/primitives/input`
  one line of text. it owns its own focus ring and its error state.
- Knob · recedes · `usva/primitives/knob`
  a value you turn to rather than slide to. the thumb lies flat in the ring until you take hold of it.
- Label · recedes · `usva/primitives/label`
  names a control and clicks through to it. never decorate a heading with it.
- List · recedes · `usva/primitives/list`
  an ordered or unordered list that a screen reader counts. dividers and markers optional.
- LoadingOverlay · recedes · `usva/primitives/loading-overlay`
  covers the region that is busy, and only that region. scroll lock is refcounted.
- LogLine · recedes · `usva/primitives/log-line`
  one machine event: a severity rail, a level, a source, and the detail if you want it.
- NotificationBadge · recedes · `usva/primitives/notification-badge`
  an unread count pinned to whatever it overlays. it wraps, it does not replace.
- Popover · recedes · `usva/primitives/popover`
  rich content anchored to a trigger, and it can be interacted with.
- Progress · recedes · `usva/primitives/progress`
  how far along a determinate task is. if you cannot say, use a spinner instead.
- Radio · recedes · `usva/primitives/radio`
  exactly one of a few. more than about five and you want a select.
  - RadioGroup · ships with Radio, same import path
- Select · recedes · `usva/primitives/select`
  one value out of many, when the list is too long to lay out flat.
- Skeleton · recedes · `usva/primitives/skeleton`
  the shape of the content before it arrives. SkeletonMirror infers it from any children.
  - SkeletonGroup · ships with Skeleton, same import path
  - SkeletonMirror · ships with Skeleton, same import path
- Slider · recedes · `usva/primitives/slider`
  a value on a continuum, where the trend matters more than the exact number.
- Spinner · recedes · `usva/primitives/spinner`
  an indeterminate wait, inline and small. the plain one, for buttons and dense UI.
  - PageLoader · ships with Spinner, same import path
- StatChip · recedes · `usva/primitives/stat-chip`
  one number in a pill: a credit balance, a quota, a count you keep glancing at.
- Switch · recedes · `usva/primitives/switch`
  a setting that takes effect immediately. no save button follows it.
- Tabs · recedes · `usva/primitives/tabs`
  peer views in one region. not steps, and never a way to hide required fields.
- Terminal · recedes · `usva/primitives/terminal`
  a shell command in a row, prompt and copy button included.
- Textarea · recedes · `usva/primitives/textarea`
  text that runs past one line. it can grow with what you type, up to a ceiling you set.
- Toast · recedes · `usva/primitives/toast`
  the result of something you just did. fired imperatively, from anywhere.
  - Toaster · ships with Toast, same import path
- ToggleChip · recedes · `usva/primitives/toggle-chip`
  a chip that stays pressed. a filter you turn on, not a tag you read.
- Tooltip · recedes · `usva/primitives/tooltip`
  names an unlabelled control. it holds no action and it is never the only copy of the truth.

### core · patterns (27) · structures

- BentoGrid · structures · `usva/patterns/bento-grid`
  cells of different weights in one grid, for a page that has to say several things at once.
  - BentoCard · ships with BentoGrid, same import path
  - BentoInfo · ships with BentoGrid, same import path
  - BentoMetric · ships with BentoGrid, same import path
  - BentoText · ships with BentoGrid, same import path
- CaseStudyHero · structures · `usva/patterns/case-study-hero`
  opens a piece of work: the title, the accent, the metadata, the link out.
- ChecklistCard · structures · `usva/patterns/checklist-card`
  a card whose body is a real list, so the reader gets the count and the order.
- CtaBanner · structures · `usva/patterns/cta-banner`
  the ask, at the end of a page. everything below the title is optional.
- DashboardGrid · structures · `usva/patterns/dashboard-grid`
  widgets the user drags and resizes into their own layout, keyboard included.
- DisclosureRow · structures · `usva/patterns/disclosure-row`
  a row that opens. the chevron and the expanded state, without the progress bar.
- EmptyState · structures · `usva/patterns/empty-state`
  nothing here yet, and here is the one thing to do about it.
- EntityCard · structures · `usva/patterns/entity-card`
  a thing with a name, a picture, some metadata and actions. stack, row or showcase.
- FeatureCarousel · structures · `usva/patterns/feature-carousel`
  features one at a time, advancing on their own, pausing when you look closer.
- FieldGroup · structures · `usva/patterns/field-group`
  the label, control, description and error of one field, wired together correctly.
- Footer · structures · `usva/patterns/footer`
  the bottom of the site: columns when there is a lot, one row when there is not.
- HeroSplit · structures · `usva/patterns/hero-split`
  copy on one side, the proof on the other. both sized off the container, not the window.
- MockupShowcase · structures · `usva/patterns/mockup-showcase`
  a browser or device frame around your media. the media is a slot, so bring your own.
- PageHeader · structures · `usva/patterns/page-header`
  the top of a screen: title, description, actions, and the stats worth reading first.
- Panel · structures · `usva/patterns/panel`
  a titled region with its own actions and its own loading state. the dashboard workhorse.
- ProgressRow · structures · `usva/patterns/progress-row`
  a label, a count and a bar, as one row in a list of many.
- Pullquote · structures · `usva/patterns/pullquote`
  one sentence, pulled out and made large, with an ornament slot beside it.
- RoadmapTimeline · structures · `usva/patterns/roadmap-timeline`
  what shipped, what is in flight, what is planned, on one connected track.
- SectionHeading · structures · `usva/patterns/section-heading`
  a mono eyebrow over a display title. the loud way to start a section.
- SectionLabel · structures · `usva/patterns/section-label`
  a numbered index, a title and a lede. the quiet way to start a section.
- SegmentedControl · structures · `usva/patterns/segmented-control`
  a few exclusive options, all visible at once. the plain one, for dense UI.
- StatBento · structures · `usva/patterns/stat-bento`
  the numbers you lead with, at display scale, counting up as they arrive.
- StatCard · structures · `usva/patterns/stat-card`
  one metric, its trend and a note on why it moved.
- StepChips · structures · `usva/patterns/step-chips`
  three or four steps in a row, with the arrows inside the list where they belong.
- StepList · structures · `usva/patterns/step-list`
  an ordered procedure, one step per row, each with room for an icon.
- StripeCard · structures · `usva/patterns/stripe-card`
  a card keyed by colour, so a long list of them sorts by eye. the stripe is data.
- Toolbar · structures · `usva/patterns/toolbar`
  the controls above a view: filters, counts, a legend, the actions on the right.

### core · motion (2) · guides

- Reveal · guides · `usva/motion/reveal`
  scroll reveals that resolve out of mist as they come into view, with a stagger group for cascading children.
  - RevealGroup · ships with Reveal, same import path
- PageTransition · guides · `usva/motion/page-transition`
  a route-level fade and lift, keyed on any routeKey.

### sula (6) · asserts

the sula rules apply to every entry below, not to some of them:

> ! one sula element per region. a region is a bounded area competing for a single attention focus, so two liquid fields in the same one fight and read as noise.
> ! different regions are fine. a frame at the viewport edge is ambient and at the boundary; a nav at the top is a focal control surface. they do not compete, and they ship together.
> ! keep it out of dense, task-bound UI. not a table, not a form, not a dashboard someone is working in.
> ! prefers-reduced-motion drops it to a static equivalent. the field is decoration over a UI that already works.

- SulaNav · asserts · `usva/sula/sula-nav`
  a navigation bar whose parts merge and pinch apart as you move between views.
- SulaSegmented · asserts · `usva/sula/sula-segmented`
  a segmented control that pinches a droplet off the old segment and merges it into the new one.
- SulaFab · asserts · `usva/sula/sula-fab`
  a floating action button whose actions emerge as liquid beads and pinch back on close.
- SulaLoader · asserts · `usva/sula/sula-loader`
  a metaball spinner for a wait you want felt. it loops only while it is mounted.
- SulaField · asserts · `usva/sula/sula-field`
  an ambient field of blobs that drift and merge slowly behind content.
- SulaFrame · asserts · `usva/sula/sula-frame`
  a liquid border that hugs a card, or the whole viewport, and leans toward your cursor.

### atmospheres (8) · room

- Utu · room · `usva/atmospheres/utu`
  fog. a slow drifting haze that sits behind everything and asks for nothing.
- Kajastus · room · `usva/atmospheres/kajastus`
  the glow on a horizon. light banked low, as if something is rising just out of frame.
- Kynnös · room · `usva/atmospheres/kynnos`
  tilled ground. a lit height field of long furrows, raked by a grazing lamp.
- Hehku · room · `usva/atmospheres/hehku`
  an ember. a knotted filament that blooms where it folds back over itself.
- Loimu · room · `usva/atmospheres/loimu`
  a flare. streamers of light drawn up through the dark and gone.
- Routa · room · `usva/atmospheres/routa`
  ground frost. low cells heave from underneath and split at their frozen seams.
- Väre · room · `usva/atmospheres/vare`
  a ripple. cosine ridges spreading across a surface you cannot quite see.
- Kuulto · room · `usva/atmospheres/kuulto`
  a shimmer through something. light arriving after it has passed through a body.

## where to look next

- `https://usva.build/docs/components/<slug>`: one page per component, props and all.
- `https://usva.build/llms.txt`: the same index with summaries, as plain text.
- `https://usva.build/design-language`: the foundations, before the components.
- `https://usva.build/design-language/tokens.json`: resolved token values as DTCG json.
- `https://usva.build/studio`: tune an atmosphere and copy its params.
