Create a skin
Re-value the token contract over an installed pack, or own a full pack of three files, then reach the component knobs beneath.
Two lanes
A skin is the token contract resolved twice — light and dark — plus optional component-level CSS. Two ways to make one yours, and the first is usually enough: re-value tokens over an installed pack, or own a full pack when the design system is yours end to end.
The theme editor in these docs drives the first lane visually — edit tokens live, audit contrast, copy the override. The AI builder produces the same shape from a prompt.
Re-value tokens over a pack
Keep the installed pack and re-value tokens in your own CSS, after the skin imports. Scope the override the way the
pack scopes itself, repeating [data-skin] — portalled surfaces re-assert data-skin on their positioner, so a
:root override never reaches them. Match the pack's weight and the later rule wins.
/* app/globals.css, after the skin imports */[data-skin="refined"][data-skin] { --primary: oklch(0.55 0.2 260); --radius: 10px;}:where(.dark) [data-skin="refined"][data-skin],.dark[data-skin="refined"][data-skin] { --primary: oklch(0.72 0.17 260);}This lane survives every update: update never touches your CSS entry, and reinstalling the pack refreshes the
pack's files without reclaiming your overrides. The theme editor's copy button emits exactly this block.
Own a full pack
A pack is three files, already in your project: skin-theme.css, skin.css, and skin.config.tsx. Start from Flat —
the smallest complete pack, 83 lines of tokens over an intentionally empty skin.css — then make it yours.
npx shadcn@latest add https://control-ui.dev/r/skin-flat.json --overwriteGive the pack its own id. It appears in three places and all three must agree: the [data-skin="…"] selectors in
skin-theme.css, the id in skin.config.tsx, and the data-skin attribute on your root element. A mismatch does
not fail loudly — the page keeps its tokens while every popover, dialog, and menu renders with none, because portals
stamp the config id on their positioner.
Then resolve the contract: 46 tokens required in both modes — the 26 core roles plus the 20 the theme editor groups under advanced (shadows, motion, overlay opacity, control geometry). Advanced means edited less often, not optional. The remaining 80 contract tokens are derived: core ships their defaults, re-value them only when the look calls for it. Tests enforce these boundaries on every shipped pack.
skin-theme.css — the full token contract, both modes, nothing else.skin.css — component-level CSS under your data-skin root; it never redeclares a contract token.skin.config.tsx — the id plus typed behavior choices and optional adornments; Flat needs only the id.Go deeper with component knobs
When tokens cannot express the look — a gradient hover, a popup family with its own border treatment — re-value the
registered --cui-* knobs in skin.css. Every visual decision in a recipe reads one, and all 441 knobs inherit, so
a value set at the family root reaches every part beneath it.
/* skin.css — one family, restyled at its root. */[data-skin="acme"] :where([data-control-family="button"][data-control="true"]) { --cui-button-radius: 999px; --cui-button-hover-bg: oklch(from var(--primary) l c h / 0.12);}Every knob — name, syntax, initial value, recipe default — is published per family in the generated
skin-contract.json, so a skin can be written or generated against the catalog instead of the recipe sources. Set
knobs at family roots: a knob set on an inner part is final and beats every inherited re-value, including inline
styles. The full resolution story is in the architecture guide's cascade contract.