Get started

Shadcn design tokens: :root to bg-primary

Shadcn hands you the theme as CSS variables and the components as code you own. That is freedom on install day and ambiguity every day after: which layer owns a color, and what exactly does a preset rewrite? This guide walks one token through every hop it travels, then applies a preset to a real project and reads the diff.

Updated August 3, 2026

Trace one token end to end

Open src/index.css (or app/globals.css in a Next.js project) in anything initialized with the current CLI and you will find the same three blocks. Everything shadcn calls a theme lives here. This is the default neutral theme a fresh radix-nova project ships, values verbatim:

:root {
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  /* …the other 20+ theme variables… */
}

.dark {
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
}

@theme inline {
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}
Scaffolded by shadcn CLI 4.15.0. Colors are oklch throughout.

The first two blocks are plain CSS custom properties. :root carries the light value; .dark overrides it whenever a dark class sits on an ancestor. The third block is Tailwind v4's bridge: @theme inline republishes each variable under the --color-* namespace, which is what makes Tailwind generate utilities from it. Delete that block and bg-primary stops existing as a class.

The last hop is in the component file, which shadcn installed into your project rather than into node_modules:

// src/components/ui/button.tsx (installed into your project)
const buttonVariants = cva("inline-flex items-center justify-center …", {
  variants: {
    variant: {
      default: "bg-primary text-primary-foreground hover:bg-primary/80",
      // secondary, destructive, ghost, …
    },
  },
})
The default variant consumes the token as a utility class. Change --primary in :root and this button repaints; change it only in .dark and just dark mode moves.

So a decision like "primary is indigo now" has exactly four places to succeed or fail: the :root value, the .dark value, the @theme inline alias, and the class in the component. When a color looks wrong, walk those hops in order and stop at the first one whose output disagrees with its input. The rest of this guide is that walk, scaled up to presets and whole components.

Give every layer one owner

A token value can appear in a design brief, in configuration, in a delivered preset, in a component, in a local override, and on screen. Those are different questions with different answers, and mixing them up produces the familiar loop of patching the button, then the theme, then the button again. Decide up front who corrects what:

What it establishesCorrection owner
Approved design decisionThe intended semantic role, mode behavior, and permitted exceptionsDesign-system owner
Theme configuration (:root, .dark, @theme inline)How the project encodes the decisionProject theme owner
Preset or registry deliveryWhich files and settings arrived, from which versionIntegration owner
Component code (components/ui)Which role each variant actually consumesComponent owner
Local exceptionA named, deliberate departure with limited scopeFeature owner
Rendered resultWhat representative consumers visibly didReviewer
Each layer answers one question. When a result is wrong, the fix belongs to the first layer that disagrees with its input.

What the CLI commands actually do

The CLI reference is short and current, and worth reading over any tutorial, including this one. As of CLI 4.x, the commands that touch tokens:

  • init sets up a project: dependencies, the cn utility, Tailwind wiring, and the CSS variable blocks above. create is not a separate flow; the reference documents it as an alias for init. Both take --template (next, vite, astro, and others), --base (base, radix, aria), and --preset.
  • apply applies a preset to an existing project. By default that is a full pass: preset configuration, CSS variables, fonts, and reinstalling detected components.
  • apply --only theme and apply --only font, added in the April 2026 partial-apply release, restrict the pass to the scoped part and leave your components alone.
  • add installs a component with its dependencies. add --dry-run lists what it would write, --diff shows the change per file, and --view prints the incoming contents. These flags are the cheap way to see a replacement before it happens.
  • preset resolve prints the preset your project currently encodes (style, base color, theme, font, radius) plus a shareable code, and preset decode <code> does the reverse for any code someone sends you.

What apply rewrote: a tested before and after

Rather than reason from the docs, I scaffolded a throwaway Vite project (radix base, nova style, neutral theme), added the button, and made two edits a real project would have: a custom --brand-glow variable in index.css, and tracking-wide appended to the button's default variant. Then I applied a preset with an indigo theme twice, first scoped, then full.

npx shadcn@latest create --template vite --base radix --preset nova
npx shadcn@latest add button
# local edits: --brand-glow in index.css, tracking-wide on the button
npx shadcn@latest apply --preset b2D0yQ7G4 --only theme
The preset code decodes (via preset decode) to style luma, theme indigo, font geist, chart emerald.

The scoped pass touched exactly one file. Every theme variable in :root and .dark moved, including the sidebar and chart scales, which count as theme even under --only theme. Both local edits survived:

--- a/src/index.css
+++ b/src/index.css
-    --primary: oklch(0.205 0 0);
+    --primary: oklch(0.457 0.24 277.023);
   --brand-glow: oklch(0.85 0.17 85);
-    --primary-foreground: oklch(0.985 0 0);
+    --primary-foreground: oklch(0.962 0.018 272.314);
     …
-    --chart-1: oklch(0.87 0 0);
+    --chart-1: oklch(0.845 0.143 164.978);
apply --only theme: 21 variable lines rewritten per mode, one file changed, the custom --brand-glow line and the edited button untouched.

Then the same preset without --only. Four more files changed. components.json flipped the style from radix-nova to radix-luma, package.json gained @fontsource-variable/inter, index.css swapped --font-sans to Inter Variable, and button.tsx was rewritten from the registry. That last one is the trap: the rewrite dropped my tracking-wide without a prompt or a warning, and the command reported success. From its point of view, updating components is the job.

Two smaller results are worth keeping. The custom --brand-glow line survived even the full pass, because apply rewrites the variables it knows rather than regenerating the file. And applying a preset whose theme already matches the project changes nothing at all; git status stays clean, so re-running an apply is safe.

Component files are the replacement surface

Anything you have edited under components/ui can be reverted by a full apply. Before one: commit, scope with --only if the theme or font is all you want, and preview a component update with add --diff instead of discovering it in the working tree.

Inspect a complete kit before mapping it

Use a public kit to see the boundary between an upstream design artifact and the project-owned checks still required after delivery.

Map one semantic role before changing the system

Start with a role whose propagation you can watch. Primary action is a good first pick: it reaches several states and usually behaves differently per mode. Do not copy a variable name from a tutorial; open the project's index.css and button.tsx and write down the identifiers they actually use. Then separate the job from the value. Values change between modes and releases while the job stays put, and "buttons" is too broad a consumer when the change should reach the primary variant but leave destructive and disabled treatments alone.

Token change record

Source version: [kit, preset, or decision version]
Semantic role: [name the project actually uses]
Light / dark expectation: [approved value or visual expectation]
Project mapping: [variable and file]
Named consumers:
  - [component, variant, state]
Allowed exceptions:
  - [surface, reason, owner]
Must not change:
  - [surface]
Observed: [dated observation, or "not yet"]
Decision: accept | revise | block
Copyable record. Replace each bracket with what you find in the consuming project, not what a tutorial says should be there.

Component specimen · Button

Ambient Sage

Live render

The button primitive in Ambient Sage, across 4 states.

Default

Hover

Focus

Disabled

A component specimen makes the consumer states concrete. It does not prove another project maps or renders them identically.

Map the dark expectation separately rather than assuming inversion. And treat an exception as something with a reason, a scope, and an owner. A surface that merely looks different today is not an exception; it is an unresolved conflict.

Run one controlled token change

A controlled change answers a narrow question: does the approved role reach its intended consumers without touching anything else? It is not an accessibility review or a regression suite. Keep those separate.

  1. 1

    Freeze the prior state

    Commit. Note the source version, the current light and dark values, and the component edits that must survive. shadcn preset resolve captures the starting configuration in one line.

  2. 2

    Write the expectation first

    Name the consumers and states that should move, and the surfaces that must not. Write them down before running anything; afterwards, an assumption reads exactly like a result.

  3. 3

    Preview, then run the bounded operation

    Use apply --only for theme or font changes, and add --dry-run or add --diff for component updates. Then run the narrowest command that does the job.

  4. 4

    Read the diff, not the success message

    Command completion means files were processed. The diff tells you whether the intended hops changed and whether a component rewrite took local edits with it.

  5. 5

    Render the named consumers

    Check them in light and dark, across the states that take different code paths: default, hover, focus, disabled, error where supported.

  6. 6

    Decide and record

    Accept when intended consumers moved and protected surfaces did not. Revise the owning layer when the decision was right but a hop is wrong. Block when something protected changed or the check has not actually been done.

Route failures to the layer that owns them

When the result is wrong, walk the chain from the top and stop at the first mismatch. A local patch further down may hide the symptom while every sibling consumer stays wrong.

  • Wrong upstream decision: the approved role or mode expectation is itself unsuitable. Send it back to the design-system owner.
  • Stale theme mapping: :root, .dark, or the @theme inline alias still carries an older value. Fix the theme layer.
  • Preset replacement: apply rewrote something you meant to keep. Restore it through review and scope the next pass with --only.
  • Component-local override: the mapping is right but a literal, selector, or variant intercepts it. Fix the component or document a deliberate exception.
  • Missing consumer: the component never references the role at all. Add the reference, and fix the record that claimed it was there.
  • Nobody looked: the code reads correctly but no one rendered the states. Leave the check open instead of accepting by inference.

The order matters because rendered similarity can lie. A component can match the approved color through a hardcoded value while bypassing the token entirely, and it will look right until the next mode switch or preset. The change record catches that structural defect while it is still cheap.

Use Ambient Sage as a bounded intake example

Ambient Sage is a public Identity Forge kit: a warm sage surface system, a vivid yellow accent used sparingly, Plus Jakarta Sans for product typography, JetBrains Mono for technical strings, and component-scale treatments, with a shadcn delivery route published alongside.

Token specimen · real values

Ambient Sage

Live render

Ambient Sage's actual tokens — the same values its exports use.

Color tokensSemantic roles with HEX / HSL / CMYK

Color tokens

Ambient Sage
light · HEX · HSL · CMYK

Core

#F3F4EF

background

H 72 · C0, 0, 2, 4

#1A1C17

foreground

H 84 · C7, 0, 18, 89

#E5E6E0

card

H 70 · C0, 0, 3, 10

#ECEEE8

muted

H 80 · C1, 0, 3, 7

#D8D9D2

border

H 68.57 · C0, 0, 3, 15

Brand

#FEE951

primary

H 52.72 · C0, 8, 68, 0

#1A1C17

primary-fg

H 84 · C7, 0, 18, 89

#E5E6E0

secondary

H 70 · C0, 0, 3, 10

#F7E464

accent

H 52.24 · C0, 8, 60, 3

#FEE951

ring

H 52.72 · C0, 8, 68, 0

Semantic

#C0392B

destructive

H 5.64 · C0, 70, 78, 25

#FFFFFF

destructive-fg

H 0 · C0, 0, 0, 0

#2D7238

success

H 129.57 · C61, 0, 51, 55

#C97D12

warning

H 35.08 · C0, 38, 91, 21

#545651

muted-fg

H 84 · C2, 0, 6, 66

Charts

#FEE951

chart-1

H 52.72 · C0, 8, 68, 0

#4A8FD4

chart-2

H 210 · C65, 33, 0, 17

#6BBF8A

chart-3

H 142.14 · C44, 0, 28, 25

#E07498

chart-4

H 340 · C0, 48, 32, 12

#E8A24B

chart-5

H 33.25 · C0, 30, 68, 9

Type scaleHeading, body, and mono in the kit's fonts

Typography

Ambient Sage

Scale: compact-product

Density: balanced

Heading · Plus Jakarta Sans · 1.875rem

Ship beautiful product faster

Subheading · Plus Jakarta Sans · 1.375rem

A warm-sage neutral-surface mobile kit with a single vivid yellow accent, flat tonal cards, and oversized display numerals.

Body · Plus Jakarta Sans · 1rem

Ambient Sage uses a near-white warm-sage canvas (#f3f4ef) with card panels distinguished only by a tonal shift to #e5e6e0, never by shadows or borders. A single vivid yellow (#fee951) is the only saturated color and appears sparingly at component scale as orbs, button fills, and focus rings. Primary data values render as oversized bold hero numerals with a small superscript unit. Typography is a friendly rounded geometric (Plus Jakarta Sans) with no uppercase and no tight tracking, while JetBrains Mono is reserved for hex codes and technical strings. Generous rounding and luminance-only contrast give the whole system a calm, minimal feel.

Mono · JetBrains Mono · 0.8125rem

npx shadcn add ambientsage.json

Aa

Plus Jakarta Sans · Heading

400500600700

Aa

Plus Jakarta Sans · Body

400500600700

ABCDEFGHIJKLM NOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz

0123456789 & @ # % →

Radius & spacingCorner radius, elevation, and spacing steps

Tokens

Ambient Sage primitives
density: balanced

Radius scale

sm · 0.375rem
md · 0.75rem
lg · 1.25rem
xl · 1.75rem

Component radius

button
card
input

Elevation

level 1
level 2
level 3
level 4

Spacing · base 4px

1x
2x
3x
4x
6x
8x
Ambient Sage supplies a concrete upstream artifact. Mapping and acceptance in the consuming project remain separate work.

What that gives you is an intake record, not a compatibility verdict. You can note that the kit and its shadcn artifact exist and what they document. Whether a particular project mapped every token, kept its customizations through the apply, and renders both modes acceptably is knowable only from inside that project.

Accept only what the diff and the render show

For your next theme change, pick one role and fill in the record before touching a preset. If you cannot name its consumers, its exceptions, and the surfaces that must not move, the project is not ready for a command whose default mode rewrites component files. Once you can, the change itself is usually the easy part: one scoped apply, one diff, one look at both modes.

Sources

  • shadcn CLI - shadcn/ui: The current CLI reference documents create as an alias for init, apply --only for partial preset application, the add --dry-run, --diff, and --view flags, and the preset resolve and preset decode commands.
  • Changelog: Partial preset apply - shadcn/ui: The April 2026 release added apply --only theme and apply --only font, which update only the scoped part of a preset and leave existing components unchanged.
  • Theming - shadcn/ui: Theme tokens are CSS variables defined in :root and .dark and republished to Tailwind through an @theme inline block, with values in oklch.
  • Introduction - shadcn/ui: Shadcn describes itself as open code and a code distribution platform, leaving the installed component source under the consuming project's control.
  • Ambient Sage Design Kit: Ambient Sage is a public kit with documented colors, typography, component-scale treatments, and a shadcn installation route.
  • Visual kits for agents that need to design well: Identity Forge's public kit gallery describes its kits as systems containing fonts, colors, tokens, and component rules.