design-systems
Guards react-basics-ui's design token system (primitive → semantic, with a legacy component tier) and the UI primitive component layer. Enforces no raw hex/rgb/px in components, components consuming s
SKILL.md.claude/skills/design-systems/SKILL.md
GitHubDesign Systems Engineer
Keep visual decisions flowing through the token system so colors, spacing, typography, and motion stay coherent and survive theme changes.
The 3-tier funnel
A one-way funnel. Each tier may only reference tiers above it. Never backward.
| # | Tier | Lives in | References | Count | Example |
|---|---|---|---|---|---|
| 1 | Primitive | src/global.css :root |
Raw values only | ~540 | --primitive-color-gold-500: #fcb711 |
| 2 | Semantic | src/global.css :root + [data-theme="dark"] |
Primitives only | ~513 | --semantic-brand-primary-default: var(--primitive-color-gold-400) |
| 3 | Component | src/global.css :root — legacy |
Semantics only | ~1530 | --component-navbar-bg: var(--semantic-surface-base) |
1 Primitive ← raw values, no var() refs
↓
2 Semantic ← intent, has a dark-theme override
↓
Components ← consume tier 2 directly
Components consume tier 2. That is the rule in this library, and it differs
from the classic three-tier model: tier 3 is legacy, kept only because
Navbar and Sidebar still read from it. Do not add --component-* tokens, and
do not route new work through tier 3 — style against --semantic-* directly.
Components NEVER consume tier 1. Enforce via the primitive-leak audit below.
The typed mirror — src/tokens/index.ts
Separate from the CSS tiers, src/tokens/index.ts exports typed objects
(colors, spacing, radius, shadows, typography, animation, zIndex)
whose values are var(--semantic-*) strings. It exists so consumers can reference
tokens from TypeScript (inline styles, props) with autocomplete instead of
retyping variable names.
It is a mirror, not a tier — it introduces no new values. A handful of its
entries still point at --component-* vars; those are legacy alongside tier 3.
When you add a semantic token, add it here too (step 4 of the checklist below).
When to add a token
A new token is a permanent vocabulary entry — it costs a dark-theme override, a TS mirror entry, and mental model. Reuse first.
- Same raw value used ≥ 2 times → tokenize as semantic.
- Single use but intent is brand-coupled or theme-sensitive → tokenize as semantic.
- Truly one-off (e.g. a duration unique to one component) → inline with a comment explaining why.
Adding a new semantic token — checklist
Do all of these in one change:
- Primitive. Pick or add the primitive holding the raw value.
- Semantic. Add the semantic token referencing the primitive via
var(). - Dark override. Redefine the semantic token inside the
[data-theme="dark"]block at the bottom ofsrc/global.css. Skip only if theme-invariant (a size, duration, or font family); leave a comment stating the skip. - TS mirror. Add to
src/tokens/index.tsso consumers get autocomplete. - Consumer. Use it. Unused tokens decay.
Skipping step 3 is the most common silent failure — light theme still works and the dark theme silently inherits the light value.
This library keys dark mode off
[data-theme="dark"], notprefers-color-scheme. There is no@media (prefers-color-scheme: dark)block inglobal.css;ThemeProvidersets the attribute. Adding an override to a media query would do nothing.
Component tokens (tier 3) — legacy, do not extend
--component-* variables are an earlier layer that gave each component its own
named variables (--component-badge-font-size-small). The library has moved off
it: every component except Navbar and Sidebar now styles against --semantic-*
directly, which is why ~1530 tier-3 tokens remain defined but almost none are read.
- Adding a new component: consume semantic tokens. Do not create
--component-*entries. - Touching
Navbar/Sidebar: migrating them to semantic tokens is welcome, but it is a visual change — verify against both themes rather than folding it into unrelated work. - Deleting unused tier-3 tokens is safe in principle but noisy; only do it as a deliberate, isolated cleanup.
If a component genuinely needs a value with no semantic equivalent, add a semantic token for it (full checklist above) rather than reviving tier 3.
Primitive components
A primitive is a reusable, domain-free UI building block (Button, Input, Dialog, Toast, Tabs, etc.).
Must contain
var(--semantic-*)references via CSS or a typed token import.- One clear public API; sub-parts live alongside with sub-component naming.
- A11y baseline: keyboard reachable, visible focus ring, correct ARIA role.
- Both light and dark behavior via tokens — never theme-detect with JS.
Must NOT contain
- Raw hex, rgb, hsl, or px values.
var(--primitive-*)references — must route through the semantic tier.- Feature-domain types or models.
- Data-layer imports (query clients, routers, fetch).
- Business rules — primitives don't know why they're used.
Audits
Run in order, from the repo root.
1. Hardcoded visual values
grep -rn "color: #\|background: #\|background-color: #" src/components
grep -rnE "(bg|text|border|h|w|gap|p|rounded)-\[#" src/components
Each hit: replace with a token, or document why this value is local.
2. Primitive-tier leaks
grep -rln "var(--primitive-" src/components
Any hit is a bug — the component is skipping the semantic tier and will not respond to theme changes.
3. Missing dark-theme overrides
For each colour token in the :root semantic block, confirm the [data-theme="dark"] block redefines it (or that it is theme-invariant). Size, spacing, duration, and z-index tokens legitimately have no dark override.
4. Dangling references
Every var(--…) a component reads must be defined, or the whole declaration is
silently dropped — no console warning, no build error. This has produced real
bugs here (see Divider, which referenced a --semantic-spacing-* family that
never existed).
# every token defined
grep -oE -- '--[a-zA-Z0-9-]+[[:space:]]*:' src/global.css | sed -E 's/[[:space:]]*:$//' | sort -u > /tmp/def.txt
# every token referenced by source (excluding tests/stories)
find src -type f \( -name '*.ts' -o -name '*.tsx' \) ! -name '*.test.*' ! -name '*.stories.*' -print0 \
| xargs -0 grep -hoE -- 'var\([[:space:]]*--[a-zA-Z0-9-]+' | sed -E 's/var\([[:space:]]*//' | sort -u > /tmp/used.txt
comm -23 /tmp/used.txt /tmp/def.txt
Expect only two kinds of survivor: prefixes built by template literal
(var(--semantic-space-${size})) and vars the component sets itself
(--table-pad-left). Resolve each dynamic prefix by hand against its value map.
Gap: there is no automated token-resolution test in this project. The audit above is the substitute. Adding
src/tokens/tokens.resolution.test.tsto assert each semantic token resolves to its expected primitive would make it enforceable.
Decision flowchart
"I need a color / spacing / radius / duration for X"
│
├─ Does a semantic token already capture this intent?
│ └─ YES → use it. Done.
│
├─ Does a primitive value already exist with this raw value?
│ └─ YES → add a semantic token referencing it (full checklist).
│
└─ Neither exists.
└─ Add primitive → semantic → dark override → TS mirror → consumer.