Theming
How the vendored Vercel theme, app-owned fonts, and project tokens form a safe three-layer theme contract.
The product app uses the shadcn.io Vercel theme. Its source artifact is
vendored at packages/ui/themes/vercel.json; the application never fetches a
tokenized registry URL at build or runtime.
Token Architecture
packages/ui/src/globals.css has three deliberately separate layers:
- Theme — the canonical light and dark values from the vendored Vercel artifact.
- Fonts — Geist and the fallback stacks owned by the application layout.
- Project extensions — semantic
success,warning, andinfotokens, plus the reduced-motion policy.
Theme swaps may replace only the canonical theme blocks. They must not clobber font wiring or project extensions.
Run the contract check after every theme edit:
bun --cwd packages/ui theme:verifyIt verifies the vendored SHA-256 and compares the canonical CSS blocks with the artifact. Font keys are intentionally excluded because the product owns them.
Color Tokens
The color system uses semantic names that map to foreground/background pairs:
| Token | Purpose |
|---|---|
--background / --foreground | Page background and default text |
--card / --card-foreground | Card surfaces |
--popover / --popover-foreground | Dropdown and dialog overlays |
--primary / --primary-foreground | Primary actions and branding |
--secondary / --secondary-foreground | Secondary actions |
--muted / --muted-foreground | Subdued backgrounds and helper text |
--accent / --accent-foreground | Highlighted interactive elements |
--destructive / --destructive-foreground | Danger states and delete actions |
--border | Borders and dividers |
--input | Form input borders |
--ring | Focus rings |
--chart-1 through --chart-5 | Data visualization palette |
Sidebar Tokens
The sidebar has its own set of tokens for independent theming:
| Token | Purpose |
|---|---|
--sidebar / --sidebar-foreground | Sidebar background and text |
--sidebar-primary / --sidebar-primary-foreground | Active item highlight |
--sidebar-accent / --sidebar-accent-foreground | Hover and interaction states |
--sidebar-border | Sidebar internal borders |
--sidebar-ring | Focus rings within the sidebar |
Changing the Theme
To install another registry theme:
- Run the theme's
bunx shadcn@latest add <registry-url>command locally. - Vendor the returned registry JSON under
packages/ui/themes/and refresh its SHA-256 file. Never commit the private registry token or URL. - Keep the resulting light/dark variables in the canonical theme blocks only.
- Restore the font and project-extension layers if the CLI rewrote them.
- Run
bun --cwd packages/ui theme:verify, typecheck, lint, and the visual baseline matrix.
The Vercel theme intentionally uses a monochrome primary. Use chart and status tokens for data semantics; do not introduce one-off palette utilities in app components.
Radius Tokens
Radii are computed from a single --radius base value:
| Token | Computed value |
|---|---|
--radius-sm | calc(var(--radius) - 4px) |
--radius-md | calc(var(--radius) - 2px) |
--radius-lg | var(--radius) |
--radius-xl | calc(var(--radius) + 4px) |
--radius-2xl | calc(var(--radius) * 1.8) |
--radius-3xl | calc(var(--radius) * 2.2) |
--radius-4xl | calc(var(--radius) * 2.6) |
Change --radius in :root to make all corners sharper or rounder at once.
The Vercel default is 0.5rem (8 px).
Typography Tokens
Three font families are defined:
| Token | Default | Usage |
|---|---|---|
--font-sans | Geist | Body text, UI elements |
--font-serif | Georgia | Long-form content (optional) |
--font-mono | System monospace stack | Code and technical data |
The product app loads Geist via next/font/google and assigns it to the
--font-geist CSS variable, which overrides the body font through the
font-sans utility. To change the app font, edit the Geist import in
apps/app/src/app/[locale]/layout.tsx.
Shadow Tokens
Shadows are defined from --shadow-2xs through --shadow-2xl, each using
subtle hsl(0 0% 0% / ...) values that work in both light and dark modes.
Spacing
The --spacing token is set to 0.25rem and powers Tailwind's spacing scale.
Changing it rescales all spacing utilities proportionally.
Using Tokens in Components
In Tailwind classes, use the semantic names directly:
// Color
<div className="bg-primary text-primary-foreground" />
// Border
<div className="border border-border" />
// Radius
<div className="rounded-lg" />
// Focus ring
<button className="focus-visible:ring-2 focus-visible:ring-ring" />In raw CSS, reference the custom properties:
.custom-element {
background: var(--primary);
color: var(--primary-foreground);
border-radius: var(--radius);
}