Guidelines

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

  1. Add the raw value to primitives.css — only if no existing primitive already carries it.
  2. 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.
  3. Expose it on the --color-* tier, which is the name product code will use.
  4. 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 configsTailwind, shadcn, Radix, cva, cmdk, tailwind-merge; also components.json, any tailwind.config.*, and a Tailwind plugin in postcss.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.