Core Principles
- Tokens as contracts — token names are stable contracts between design and code. Renaming a token is a breaking change. Value changes are semver-controlled.
- Brand layer ownership — your org's source of truth lives in
@your-org/tokens, not in Underlith core. Underlith provides the primitives. You own the brand. - Single consumption path — components consume tokens only through
var(--ul-color-*). No literal values. The bridge from canonical tokens to aliases lives in one place. - Semantic versioning — major for name changes or removals, minor for new tokens, patch for non-critical value adjustments.
- Auditability — all changes go through a PR with justification, changelog entry, and assigned owner.
- Agent-readiness — tokens must be consumable by AI coding agents without ambiguity. Names must be self-describing. No two tokens should resolve to the same semantic intent.
Three Levels of Governance
Level 1 — Underlith core
Base primitives: space, type, radius, motion, opacity, breakpoints, elevation, status.
Changes here affect every consumer. Treat as infrastructure — stable, slow-moving, semver-strict. No org-specific values live here.
Level 2 — Brand package (@your-org/tokens)
Your org's brand tokens and semantic aliases. Generated by underlith brand init.
This is the contract between design intent and every product your org builds. Changes here affect every surface. Publish a new version and every project updates on npm update @your-org/tokens.
Level 3 — Product consumers
Each product — web app, mobile, admin, website, AI agent — installs @your-org/tokens and inherits the contract. Products do not define colors. Products do not override tokens locally. If a product needs a variant, the variant is a new token, not a local override.
Underlith core → @your-org/tokens → Every product
(slow, stable) (your cadence) (npm update)
(primitives only) (brand contract) (components, agents)Roles and Responsibilities
- Token owners — one owner per category: Colors, Typography, Spacing, Motion, Status. Responsible for reviewing changes and maintaining the changelog.
- Design team — defines semantic intent. Validates that token names match design language. Approves changes that affect visual identity.
- Integration team — maintains export pipelines (CSS/JSON), CI jobs, and the token drift check.
- Product teams — consume
@your-org/tokens. May request new tokens via PR. May not define literal values in components. - AI agents — treated as product consumers. Must receive the same token contract. Agent-generated code is subject to the same lint rules as human-written code.
Change Process
Underlith core changes
- Open a PR with description, category, and assigned owner
- Run automated validations — schema, breaking-change detection, token-aware lint
- Review and approval by category owner and design team when applicable
- Merge and release — publish
@mikaelcarrara/underlith - Downstream brand packages update on their own schedule
Brand package changes
- Update token values in
underlith.tokens.cssor re-rununderlith brand init - Review with design team — validate semantic intent, not just the value
- Bump version following semver
- Publish —
npm publish --access public - Every product updates on
npm update @your-org/tokens
Adding new tokens
New tokens start as a proposal: name, semantic intent, initial value, and which products consume it. A token without a clear consumer is not ready to ship.
Naming rules:
- Canonical:
--ul-{category}-{intent}— e.g.--ul-status-success - Consumption alias:
--ul-color-{intent}— e.g.--ul-color-status-success - No abbreviations. No generic names like
--ul-color-1.
Deprecation policy
Mark tokens as deprecated in files and changelog. Maintain compatibility for at least two minor releases before removal. Removal requires a major version bump.
Token Drift
Token drift happens when a component defines a color, spacing, or typography value outside the token system — via a literal value or a local CSS variable not mapped to @your-org/tokens.
Drift is the primary sign that governance is failing.
What drift looks like
/* ❌ Drift — literal value in a component */
.badge {
background-color: #d3fd54;
color: #000;
}
/* ✅ Governed — token alias */
.badge {
background-color: var(--ul-color-primary);
color: var(--ul-color-primary-foreground);
}Automated drift detection
CI runs a lint job that scans component files for literal color values, hardcoded spacing, and CSS variables not prefixed with --ul-. Any match fails the build.
The lint:tokens-check script scans for:
- Hex, rgb, hsl, oklch literals outside token definition files
var(--references not resolving to--ul-*or--ul-color-*- Font family declarations not using
--ul-font-*
# .github/workflows/token-drift.yml
name: Token Drift Check
on:
pull_request:
paths:
- 'src/**'
- 'web/**'
- 'styles/**'
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint:tokens-checkCI and Automation
For the full automation narrative, examples and token‑aware prompts, see AI & Agents. This .html page tells the story; the .md files remain canonical for rules and policies.
| Job | Purpose |
|---|---|
build:tokens |
Generate CSS from canonical source |
lint:tokens |
Validate schema and detect breaking changes |
lint:tokens-check |
Detect hardcoded values and unmapped variables in components |
visual-regression |
Ensure replacements do not alter appearance |
test:contrast |
Validate status and brand tokens meet WCAG AA contrast minimums |
audit-log |
Record generations and refactors for review |
name: Tokens CI
on:
pull_request:
paths:
- 'src/tokens/**'
- 'styles/**'
- 'package.json'
jobs:
build-tokens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build:tokens
- run: npm run test:tokens
drift-check:
runs-on: ubuntu-latest
needs: build-tokens
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint:tokens-check
contrast-check:
runs-on: ubuntu-latest
needs: build-tokens
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:contrast
visual-regression:
runs-on: ubuntu-latest
needs: drift-check
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run visual-regressionRelease Artifacts
| Artifact | Description |
|---|---|
underlith.tokens.css |
Canonical token definitions as CSS Custom Properties |
underlith.css |
Tokens + base styles, main entry point |
your-org.brand.css |
Brand layer, generated by underlith brand init |
your-org.base.css |
Base layer, generated by underlith brand init |
underlith.tokens.json |
Tokens in JSON — enables Figma sync, Style Dictionary (roadmap) |
CHANGELOG.md |
Change history and deprecation instructions |
Accessibility and Motion Policy
- All transitions and animations must use
--ul-duration-*and--ul-ease-*tokens. - Prefer composite motion tokens (e.g.
--ul-motion-skeleton) when available. - Motion must not be required for core functionality.
- Respect
prefers-reduced-motion: reduceby collapsing durations to near-zero.
@media (prefers-reduced-motion: reduce) {
:root {
--ul-duration-nano: 0ms;
--ul-duration-micro: 0ms;
--ul-duration-fast: 0ms;
--ul-duration-base: 0ms;
--ul-duration-moderate: 0ms;
--ul-duration-slow: 0ms;
--ul-duration-glacial: 0ms;
--ul-duration-epic: 0ms;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Signs the Governance is Working
- Design changes go through
@your-org/tokens, not component tickets - Token names are used in conversation — not hex values or pixel numbers
- A single
npm updatepropagates brand changes across every product - AI agents generate code that passes lint on the first run
- No hardcoded values survive CI
Signs Something Went Wrong
- A component was changed directly without updating a token
- Two products have different shades of the "same" brand color
- A breaking token change shipped without a major version bump
- Figma and code have different names for the same decision
- An agent introduced a literal color value that passed review unnoticed
For ongoing token health and long-term ownership, see STEWARDSHIP.md.
For consumption rules and alias strategy, see Consumption strategies.