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);
}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, …
},
},
})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 establishes | Correction owner | |
|---|---|---|
| Approved design decision | The intended semantic role, mode behavior, and permitted exceptions | Design-system owner |
| Theme configuration (:root, .dark, @theme inline) | How the project encodes the decision | Project theme owner |
| Preset or registry delivery | Which files and settings arrived, from which version | Integration owner |
| Component code (components/ui) | Which role each variant actually consumes | Component owner |
| Local exception | A named, deliberate departure with limited scope | Feature owner |
| Rendered result | What representative consumers visibly did | Reviewer |
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:
initsets up a project: dependencies, thecnutility, Tailwind wiring, and the CSS variable blocks above.createis not a separate flow; the reference documents it as an alias forinit. Both take--template(next, vite, astro, and others),--base(base, radix, aria), and--preset.applyapplies a preset to an existing project. By default that is a full pass: preset configuration, CSS variables, fonts, and reinstalling detected components.apply --only themeandapply --only font, added in the April 2026 partial-apply release, restrict the pass to the scoped part and leave your components alone.addinstalls a component with its dependencies.add --dry-runlists what it would write,--diffshows the change per file, and--viewprints the incoming contents. These flags are the cheap way to see a replacement before it happens.preset resolveprints the preset your project currently encodes (style, base color, theme, font, radius) plus a shareable code, andpreset 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 themeThe 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);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 | blockComponent specimen · Button
Ambient Sage
Live renderThe button primitive in Ambient Sage, across 4 states.
Default
Hover
Focus
Disabled
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
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
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
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
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
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
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 inlinealias 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 renderAmbient Sage's actual tokens — the same values its exports use.
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.