Working with tokens
One tier is public. The other two are the foundation's business, and reaching into them is a hard failure.
The rule
Product code may name --color-*, --space-*, --radius-*, --border-*, --text-*, --font-* and --icon-* — and nothing above them. The semantic and primitive tiers belong to src/styles/.
The reason is not tidiness. A rule bound to a primitive violet stays violet on the green CMS surface: reaching past the public tier pins a component to a value rather than a role, and silently opts it out of the contextual accent.
Why mistakes here are silent
An undefined custom property drops its whole declaration. background: var(--typo) does not error, does not warn, and does not fall back — the element simply has no background. Nothing in the browser or the build will tell you.
That is not hypothetical: --control-hover-bgwas consumed seventeen times in this codebase while never being declared anywhere. The audit's resolution check exists because of it.
The corollary is that a fallback is worse, not safer — var(--gone, 13px) ships the literal forever while reading like a token. See Anti-patterns.
Adding a token
- Add the raw value to
primitives.css— only if no existing primitive already carries it. - Give it meaning in
semantic.css. If it should track the contextual accent, it must also be restated inside the[data-accent]block — declaring the source is not enough, because properties inherit as computed values. - Expose it on the
--color-*tier, which is the name product code will use. - Verify by measuring. Open the surface, read
getComputedStyle(el).getPropertyValue("--your-token")in each accent scope, and confirm it changes. Do not verify by eye.
Two files can declare the same :root token, and load order decides the winner — index.css imports semantic.css before utilities.css, so a duplicate in utilities wins while the semantic one still reads as the source of truth. Scoped re-declaration is a different thing and is the correct pattern; the test is whether both declarations sit at :root.
What the audit checks
npm run design:audit holds several lines at zero, and these are the ones that must never regress:
- Token resolution — every
var(--token)resolves, with or without a fallback. - Tier — no semantic or primitive reference outside
src/styles/, in CSS or in a JSX style object. - Contrast — WCAG ratios on the text tiers.
- Banned dependencies and configs — Tailwind, shadcn, Radix, cva, cmdk, tailwind-merge; also
components.json, anytailwind.config.*, and a Tailwind plugin inpostcss.config.*. A shadcn config survived here for months because the guard read only dependencies and imports. - Stale allowlist entries — an allowlist pointing at a file that no longer exists silently widens the guard the moment something is created at that path.
Raw-value findings are a separate, tracked burn-down and sit at a known baseline. The checks above sit at zero.