Learning Track
Filter content by your role to see the most relevant sections
Color Foundations
Build a color system that survives theming, rebranding, and accessibility audits: core palettes, semantic roles, WCAG contrast as a computed property, and mode-aware tokens that ship to CSS cascade layers.
Why This Matters
Color is the foundation most likely to be treated as decoration, and the one where that mistake is most expensive. Every hardcoded hex value in a component is a decision that must be re-made by hand for dark mode, for each brand, and for every future accessibility audit. A tokenized color system converts those one-off decisions into a small set of named, versioned contracts.
Done well, the color layer is what makes everything else cheap: theming becomes data instead of a rewrite, contrast becomes a computed property you can validate in CI rather than a claim you re-verify by eye, and a brand change becomes a mapping change instead of a find-and-replace across the codebase.
This page walks through the color system as it is actually built in this repository—core palettes, semantic roles, component-level tokens, and the generated CSS that ties them together—so the concepts stay attached to working code you can inspect.
Core Concepts: The Three Layers of a Color System
A production color system is not one palette. It is three layers, each with a different job and a different rate of change:
- Core primitives — the raw material: ramped palettes, mode constants, and data-visualization scales. Values live here; meaning does not.
- Semantic roles — intent-bearing aliases like
foreground.primaryandaction.background.primary. This is where meaning lives, and the only layer product code should reference. - Component tokens — scoped consumption contracts like
--ds-card-color-background-defaultthat give each component a narrow, stable interface to the layers below.
Primitives: Ramps, Not Swatches
The core layer in ui/designTokens/core/color.tokens.json defines nine ramped families—brand.primary, neutral, red, orange, yellow, green, blue, teal, and violet—each in eight steps from 100 (lightest) to 800 (darkest). A ramp is a calibrated ladder, not a pile of favorite swatches: the point of steps is that 500 and 600 relate to each other the same way in every family, so a hover state can be expressed as "one step darker" as a system rule.
Two things about this file surprise people coming from tool-palette exports. First, values are stored in the W3C Design Tokens (DTCG) format with structured color values, not hex strings:
// ui/designTokens/core/color.tokens.json (excerpt)
"blue": {
"600": {
"$value": {
"colorSpace": "srgb",
"components": [0.0, 0.2588, 0.8627]
},
"$description": "Level 600 of the blue color scale.",
"$type": "color"
}
}Hex is an output encoding, produced during generation—the same source can emit SCSS, CSS custom properties, or type definitions without a second source of truth. Second, the families overlap on purpose: blue mirrors brand.primary. The brand ramp is what semantics reference; the named hue ramps exist so semantic decisions can be described and swapped in human terms ("make the accent teal") without touching consumers.
The ramps are not curated swatches; they are generated, and the generation is the most important provenance fact in the color system. Each ramp is produced with the Adaptive-DS-Colors generator: the first step is keyed at 1.15:1 contrast against the background, steps are interpolated uniformly in luminance up to a bounded top end near 18:1, and the same per-level contrast targets apply to every family. Verify it against the shipped hexes: neutral and blue land within ±0.07 of each other at every level—1.15, 1.57, 2.22, 3.23, 4.88, 7.46, 11.4, 16.1:1 against white.
Read that progression as a ladder of thresholds, because the anchors put each level next to a job: 400 (≈3.2:1) brackets the WCAG UI-graphics floor, 500 (≈4.9:1) sits just above the 4.5:1 body-text floor, 600 (≈7.5:1) clears AAA. "Which step for text?" is therefore not a taste question—the ramp answers it. What remains chosen is the family hue, the step count, and the anchors themselves; everything between them is derived, which is why a brand ramp can be regenerated under new anchors without re-curating eight colors by hand.
Alongside the ramps sit two smaller groups. color.mode holds the constants every theme needs—black, white, light (#fafafa), dark (#141414), transparent. And color.datavis ships perceptually uniform colormap ramps—viridis, magma, plasma, inferno, cividis—plus categorical and divergent scales, because charts have different constraints than UI chrome: magnitude must map to perceived magnitude, not to raw channel values.
Semantic Roles: Name the Job, Not the Hue
The semantic layer in ui/designTokens/semantic/color.tokens.json contains no new colors. Every entry is a reference into the core layer, grouped by the job the color does: foreground, background, border, action, feedback, status, syntax, data, navigation, gradient, overlay, and selection. The naming rule is the contract: role, then property, then state. action.background.primary.hover reads as a sentence;blue-600 does not.
Here is the primary text color as it is actually defined:
// ui/designTokens/semantic/color.tokens.json (excerpt)
"foreground": {
"primary": {
"$type": "color",
"$value": "{color.mode.dark}",
"$extensions": {
"design.paths.light": "{color.mode.dark}",
"design.paths.dark": "{color.mode.light}"
},
"$description": "Primary foreground color"
}
}Read the inversion carefully, because it is the whole trick: light mode uses the constant named dark, and vice versa. The base $value says what this token is; the design.paths extensions say which core reference resolves in each mode. Text stays near-black on light surfaces and near-white on dark surfaces without the consumer knowing anything about modes. Action states work the same way by walking a ramp:
// Semantic action tokens ride the brand ramp
"action": {
"background": {
"primary": {
"default": { "$value": "{color.palette.brand.primary.500}" },
"hover": { "$value": "{color.palette.brand.primary.600}" },
"active": { "$value": "{color.palette.brand.primary.700}" },
"disabled": {
"$value": "{color.palette.neutral.200}",
"$extensions": {
"design.paths.light": "{color.palette.neutral.200}",
"design.paths.dark": "{color.palette.neutral.700}"
}
}
}
}
}In the default brand, brand.primary resolves to the red family, so the emitted default for --semantic-color-action-background-primary-default is #d9292b (red.500), hover is #ae0001(red.600), active is #7b0000 (red.700). The semantic layer never names a hue—it names a position on a ramp, and the brand layer decides which ramp that is.
Component Tokens: The Consumption Contract
The third layer lives next to each component: ui/components/Card/Card.tokens.css and its siblings. Each file is scoped with [data-ds-component="Card"] and prefixed --ds-card-*, and each variable resolves a semantic token with a literal fallback:
/* ui/components/Card/Card.tokens.css (excerpt) */
[data-ds-component='Card'] {
--ds-card-color-background-default:
var(--semantic-color-background-primary, #ffffff);
--ds-card-size-radius-medium:
var(--semantic-shape-card-radius, 16px);
}The fallback is a resilience contract, not decoration: components stay renderable even if the global token stylesheet fails to load, and the fallback value makes the component's assumption about the semantic layer observable at runtime. Component styles may only reference these scoped variables—reaching past them to --semantic-* or --core-* directly is the boundary the linters (npm run tokens:lint) exist to catch.
Contrast Is a Computed Property
Contrast is where color systems earn their keep, because contrast between two colors is a function, not an opinion. WCAG 2.1 defines it from relative luminance: (L1 + 0.05) / (L2 + 0.05). The thresholds this system validates against live in utils/accessibility/tokenValidator.ts:
- AA normal text: 4.5:1 — the floor for body copy
- AA large text and UI: 3.0:1 — headings, icons, borders that must be seen
- AAA normal text: 7.0:1 — long-form reading
- AAA large text: 4.5:1
Applying the formula to the semantic pairs this system actually ships (rounded to one decimal):
foreground.primaryonbackground.primary: ≈18.4:1 in light (#141414on#ffffff), ≈20.1:1 in dark (#fafafaon#000000) — clears every threshold with room to spare.foreground.secondaryonbackground.primary: ≈7.5:1 light (#555555), ≈9.5:1 dark (#aeaeae) — AAA, because secondary does not mean barely legible.- The accent as text (
red.500light,red.400dark) on primary backgrounds: ≈4.9:1 and ≈6.5:1 — passes AA, fails AAA. Accent-colored body text is therefore a deliberate, bounded choice, not a default.
Notice what that last bullet implies: the same accent that is fine for a large call-to-action label is not acceptable for a paragraph. Systems encode this by pairing roles—if you want red prose, you need a foreground.action role whose ramp position was chosen for text, not a borrowed button background.
One honest note on where the industry is heading: APCA (Accessible Perceptual Contrast Algorithm) models perceptual contrast better than WCAG 2.1's relative luminance, especially for dark surfaces. This repository's validators compute WCAG 2.1 ratios; treat APCA as the direction the math is moving, not something this system currently enforces.
Modes and Brands Are Data, Not Forks
Everything above—the design.paths extensions, the ramp references—exists so that themes can be data. The build composes the token JSON into app/designTokens.scss with an explicit layer order:
/* app/designTokens.scss — generated, do not edit */
@layer core, semantic, theme, brand, density;
@layer core {
:root {
--core-color-mode-black: #000000;
--core-color-mode-white: #ffffff;
--core-color-mode-light: #fafafa;
--core-color-mode-dark: #141414;
--core-color-palette-brand-primary-500: #0a65fe;
/* ...316 core custom properties... */
}
}The theme layer emits .light and .dark class scopes plus an @media (prefers-color-scheme: dark) block, so a page can follow the system setting or be pinned explicitly. The brand layer emits [data-brand] scopes—ten of them ship today (canary, corporate, default, forest, midnight, monochrome, ocean, rose, slate, sunset). A brand file is tiny because it only decides three things: its name, which family brand.primary points at, and a density. Ocean is { name: "ocean", accent: "teal", density: "spacious" }—its accent resolves to teal.500 in light and teal.400 in dark. Midnight swaps in violet with a tight density. No component file changes; the cascade does the work.
System Roles: Where Color Decisions Land
The three layers only stay healthy if each discipline makes decisions in its own layer and hands the next layer a contract:
Design Impact
Designers own the ramps and the semantic intent. The ramp is where craft lives: even spacing in perceptual (not channel) space, a 500 that passes AA on white across families, a 600 that is one honest step darker. The semantic layer is where judgment lives: is this border decorative or structural? Should this state be feedback.warning or status.caution? In Figma, the same structure appears as variable collections with modes—collections mirror core ramps, modes mirror light/dark—so the file a designer paints with is shape-compatible with the JSON the build consumes.
Engineering Impact
Engineers own the pipeline: DTCG sources compose into semantic and brand layers, generate the cascade-layered stylesheet, and ship typed outputs. The rule that keeps the system refactorable: product code touches only scoped component tokens. When a component reads var(--ds-card-color-background-default), the card does not know what theme is active, and theme work never requires touching the card.
Accessibility Impact
Accessibility owns the pass/fail lines and gets automation for them: validateColorPair in utils/accessibility/tokenValidator.ts computes real ratios against the WCAG thresholds, so "this pair passes AA normal" is a checkable claim, not a vibe. The accessibility posture of the whole system reduces to one rule: pair roles, not colors—if a foreground role exists, its backgrounds are enumerated, and the validator walks those pairs.
Governance Impact
Governance owns the rate of change. Core ramps change rarely and carry visual review; semantic roles change more often and carry intent review; component tokens change freely within their scope. The linters make the boundary observable: scripts/check-token-references.mjs flags references that skip layers, and scripts/check-kebab-token-vars.mjs keeps naming mechanical. A color system without those tripwires regresses to swatches by attrition.
Design & Code Interplay
On the design side, the token system shows up as structure you can paint with. A Figma library that mirrors the three layers has:
- One variable collection per core ramp — nine collections of eight steps each, named to match the token paths (
brand.primary/100…800), so a designer saying "600" and a token file sayingbrand.primary.600are the same statement. - Modes, not duplicates — semantic colors are Figma variables with light and dark modes bound to the ramp steps, exactly like
design.paths.lightanddesign.paths.dark. Switching the file to dark mode must never be a re-paint. - Intent names in styles — component styles reference semantic variables ("background/primary"), so a file audit can spot off-system color instantly: any raw swatch in a layer is a defect by definition.
The contrast tooling lives here too. Plugins that compute WCAG ratios against the thresholds (AA 4.5:1, large 3.0:1) let designers check pairs like foreground.secondary on background.primary (≈7.5:1 light) while choosing colors, not after. The numbers designers see in the tool are the same numbers tokenValidator.ts enforces in CI, because both implement the same formula on the same source values.
On the code side, the same decisions travel a deterministic pipeline. Sources are DTCG JSON; the build composes core, semantic, and brand layers; generation emits CSS custom properties with mechanical names.
The naming transform is worth internalizing:
// utils/designTokens/generators/generateCSSTokens.mjs (behavior)
// 1. Namespace by path: color.(mode|palette|datavis) → "core."
// everything else → "semantic."
// 2. Dots → hyphens, camelCase → kebab-case
// 3. Emit as CSS custom properties
color.palette.blue.600 → --core-color-palette-blue-600: #0042dc
color.foreground.primary → --semantic-color-foreground-primary
motion.duration.extraLong1 → --core-motion-duration-extra-long1: 1500ms
effect.backdropBlur.sm → --core-effect-backdrop-blur-sm: 4pxConsumers should recognize every variable's layer instantly from its prefix, and the kebab rule means a token's JSON path and its CSS name are never a mystery to each other. The composed output then lands in cascade layers with an explicit precedence:
/* Order defines precedence: later layers win */
@layer core, semantic, theme, brand, density;
@layer semantic {
:root {
--semantic-color-foreground-primary: var(--core-color-mode-dark);
--semantic-color-action-background-primary-default:
var(--core-color-palette-brand-primary-500);
}
}
@layer theme {
.dark {
--semantic-color-foreground-primary: var(--core-color-mode-light);
}
@media (prefers-color-scheme: dark) {
:root { --semantic-color-foreground-primary: var(--core-color-mode-light); }
}
}Layer order is the whole theming architecture in one line: core can never override semantic, themes override semantics, brands override themes, and density sits on top. Because precedence is declared rather than emergent from selector specificity, adding the ocean brand or a dark mode is additive—you emit another scoped block, you never edit existing rules or bump specificity in components.
Finally, components consume through their scoped contract with a literal fallback, which doubles as a runtime observability point:
/* A component style never reaches past its own scope */
[data-ds-component='Card'] {
--ds-card-color-background-default:
var(--semantic-color-background-primary, #ffffff);
}
.card {
background: var(--ds-card-color-background-default);
/* If the global sheet fails, the card still renders; if you inspect
and see #ffffff instead of a var(), you know the sheet is missing. */
}Applied Example: Shipping a Warning Border Pair
Let's ship one real decision end to end: teams need a warning border for destructive-adjacent form states, and the current feedback.border.warning resolves to #824500 in light mode. Walk the full path—ramp, alias, generation, validation, consumption—because every color decision in this system is these five steps.
Step 1: Check the ramp before touching it
The need is "a warning border that reads on both light and dark surfaces." Orange is the conventional warning hue, and the core orange ramp already has eight calibrated steps. Resist adding a new color: if the ramp lacks a usable step, the fix is usually re-spacing the ramp, not bolting on an outlier that will haunt every future "one step lighter" rule.
Step 2: Alias with both paths
The semantic entry references positions, and declares how it resolves per mode:
// ui/designTokens/semantic/color.tokens.json
"feedback": {
"border": {
"warning": {
"$type": "color",
"$value": "{color.palette.orange.700}",
"$extensions": {
"design.paths.light": "{color.palette.orange.700}",
"design.paths.dark": "{color.palette.orange.300}"
},
"$description": "Border for warning states; AA against both surfaces"
}
}
}The dark path jumps four steps, not one—on near-black surfaces a 700-step orange disappears. Mode pairs are chosen for contrast parity, not for symmetry, and that asymmetry is exactly the knowledge the token now carries forever.
Step 3: Generate and read the diff
npm run tokens:build
/* app/designTokens.scss (generated diff) */
@layer semantic {
:root {
+ --semantic-color-feedback-border-warning: #a86a00;
}
}
@layer theme {
.dark {
+ --semantic-color-feedback-border-warning: #ffc458;
}
}The build is the only writer of this file. Reviewing generated output is not busywork—it is where a wrong reference (say, aliasing orange.100 by typo) becomes visible as a value that could never pass contrast.
Step 4: Validate contrast in CI
The claim "AA against both surfaces" is checkable with the repo's own validator vocabulary:
// utils/accessibility/tokenValidator.ts (excerpt)
export const WCAG_LEVELS = {
AA_NORMAL: 4.5,
AA_LARGE: 3.0,
AAA_NORMAL: 7.0,
AAA_LARGE: 4.5,
};
// contrastRatioHex(fg, bg) → (L1 + 0.05) / (L2 + 0.05)
// A border is UI: it must clear AA_LARGE (3.0:1) against
// background.primary in BOTH modes — light #ffffff and dark #000000.A three-to-one bar sounds low until you check both modes: a border that clears 3:1 on white can fail on black. That is the recurring lesson of token-level accessibility—every pair is a per-mode claim.
Step 5: Consume through the component contract
/* ui/components/TextField/TextField.tokens.css */
[data-ds-component='TextField'] {
--ds-text-field-border-warning:
var(--semantic-color-feedback-border-warning, #824500);
}
/* The component knows a warning border exists; nothing else. */
.textFieldWarning {
border-color: var(--ds-text-field-border-warning);
}Notice what never happened: nobody wrote a hex in a component, nobody branched on dark mode in a component, and when the brand switches to ocean—teal accent—the warning border stays orange, because feedback semantics reference the orange ramp, not the brand ramp. Five steps, and the decision is now durable infrastructure instead of a style line.
Constraints & Trade-offs
Every choice below was made to keep the system cheap to change in the directions it actually changes. Naming the tradeoff is the point—if you cannot say what a rule costs, you cannot tell when the rule is wrong.
- Ramp width (8 steps): wide enough that states can be "one step away," narrow enough that choosing is still a decision. More steps would push selection cost onto every usage; fewer would force semantic roles to share steps, which is how two intents end up visually identical.
- Semantic granularity: a role per intent (
feedback.warningvsstatus.caution) makes theming precise but multiplies the matrix that must be validated. The working rule: split roles when the intents theme differently or fail differently; merge them when they do not. - Two themes, enforced: shipping exactly light and dark keeps every pair checkable in CI. A third high-contrast theme is additive (one more paths layer) but grows every pair's validation from two claims to three—budget for the audit, not just the values.
- Perceptual ramps vs brand fidelity: the ramps are contrast-anchored derivations, not hand-picked colors, but a brand guideline can still mandate a specific flagship hex. The escape hatch honors both: pin
brand.primary.500to the brand's value and re-derive the neighbors with the generator under the same anchors—never a hex in a component. - Fallbacks in component tokens: the literal in
var(--semantic-…, #ffffff)buys resilience when the global sheet fails and costs drift risk—the literal can silently age away from the token. Treat fallback updates as part of the token change, checked in the same review. - sRGB as the working space: stored components are srgb because every target (CSS, Figma) consumes it without conversion. Wide-gamut display targets would add a color-space decision at generation time—the DTCG structure already supports it; nothing in the semantic layer would change.
Common Pitfalls & Failure Modes
These are the failure modes that recur in every color system's first two years, each with the repair that fits this architecture:
1. Hex values in component styles
// Bad: The component now owns a theming decision
.card { border-color: #824500; }
// Good: The component owns an interface; the system owns the value
.card { border-color: var(--ds-card-border-warning, #824500); }One hex looks harmless; the tenth makes dark mode a re-audit of the codebase. The linter exists because review cannot catch these reliably at scale.
2. Validating only the light theme
// Bad: A pair checked once, shipped twice
validateColorPair(fg.secondary, bg.primary); // light: 7.5:1 ✓
// Good: Every pair is a per-mode claim
validateColorPair(fg.secondary, bg.primary, 'light'); // 7.5:1 ✓
validateColorPair(fg.secondary, bg.primary, 'dark'); // 9.5:1 ✓Dark mode failures cluster exactly where light mode is strongest: mid-ramp colors that pass easily on white and vanish on black.
3. Components reaching past their contract
// Bad: The card is now coupled to global structure
.card { background: var(--semantic-color-background-primary); }
// Good: Scoped indirection keeps the system refactorable
.card { background: var(--ds-card-color-background-default); }The scoped variable is two lines of insurance: it is the only thing that lets semantic tokens be renamed or re-layered without a component-by-component hunt.
4. Borrowing a background ramp for text
// Bad: red.500 passes AA on white (≈4.9:1) — as a BUTTON fill —
// and fails as body text the day someone reads a paragraph
.bodyAccent { color: var(--core-color-palette-red-500); }
// Good: Text needs a ramp position chosen for text
.bodyAccent { color: var(--semantic-color-foreground-action); }The numbers make it concrete: the accent clears 4.5:1 on white but not 7:1, and dips further on tinted surfaces. Roles exist so the system, not each author, remembers which threshold applies.
5. Alpha layered on tokens
// Bad — Opacity over a resolved color: contrast is now
// a function of whatever happens to be underneath
.scrim {
background: var(--semantic-color-overlay-scrim);
opacity: 0.5;
}
// Good: Bake alpha where it can be tracked — the core layer has
// explicit alpha-carrying colors (color.mode.transparent,
// color.opacity.50) so the composited value is the tokenOnce alpha composites at usage time, the same token renders differently over every surface and no validator can speak to the result. Alpha is a value decision; make it in the value layer.
6. Treating the generated file as editable
/* Bad: app/designTokens.scss says AUTO-GENERATED for a reason:
a hand edit is a change with no source, no diff review,
and no survival of the next build */The generated stylesheet is an artifact with a source of truth upstream. If something is wrong in the output, the fix lands in the JSON or the generator—anything else is shadow state.
Color System Health Metrics
A color system's health is measurable, and the measurements are cheap enough to run continuously. Three signals cover most of the risk:
Signal 1: Contrast debt
- Healthy: every declared semantic pair validates against its target level in every shipped mode; validator output is empty and stays empty in CI.
- Warning: pairs pass AA but sit between 4.5:1 and 7:1 where a guideline (or a customer contract) expects AAA—the accent-as-text case is the canonical example, fine until someone sets long-form copy in it.
- Critical: any pair below 4.5:1 for body text or 3:1 for UI in any mode. This is a shipping defect, not technical debt—the fix outranks feature work.
Signal 2: Off-system color
- Healthy: zero raw hex or rgb literals in component styles;
tokens:lintand a grep for hex patterns inui/components/**/*.cssboth come back clean. - Warning: literals appear only inside component token fallbacks, where they are tracked drift risk, not decisions.
- Critical: literals in component rules or inline styles—the migration has stalled, and every new screen adds to the re-audit surface.
Signal 3: Theme parity
- Healthy: every semantic token resolves in every shipped mode and brand; a missing
paths.darkentry fails the schema and validators at build time, not in a screenshot review. - Warning: parity exists but is manual—brands were checked when added and not since; the next brand is where this becomes critical.
- Critical: a mode or brand where tokens fall back to light values—users see it as "the dark theme is broken," and it is usually one missing
design.pathsextension away.
The pattern across all three: healthy is when the claims are machine-checkable and checked. The moment a color claim can only be verified by opening a file and squinting, the system has started becoming a palette.
Migration Strategy: From Hard-Coded Hex to Tokens
Legacy surfaces are mostly hex soup, and the migration that works is mechanical, staged, and boring on purpose:
- Inventory before editing. Extract every literal (hex, rgb, named colors) from component styles with their file and line. The list is the project plan; dedupe it and most systems discover they use far fewer distinct colors than anyone feared.
- Map each literal to ramp position. For each distinct color, decide: which family, which step, which semantic role should own it? Colors with no defensible role are the migration's actual payoff—delete or absorb them now.
- Add missing semantic tokens first. Land every new alias and its
pathsextensions in the token sources and rebuild, so the generated sheet carries the full target vocabulary before any component changes. - Migrate by component, highest-traffic first. Swap literals for scoped component tokens one component at a time; each component merge is independently revertible, and visual diffs stay reviewable.
- Turn the linters on as you go. Enable the layer-boundary and naming checks per directory as it migrates, so regression is blocked where cleanup has happened rather than where it has not.
A codemod sketch for the mechanical core of step 4:
// Map of literal → scoped component token, built in step 2
const literalToToken = {
'#824500': '--ds-text-field-border-warning',
'#d9292b': '--ds-action-background-primary-default',
// ...
};
// Mechanical pass: replace literals that have a mapping,
// and ONLY literals that have a mapping
for (const file of componentCssFiles) {
let css = read(file);
for (const [literal, token] of Object.entries(literalToToken)) {
css = css.replaceAll(literal, `var(${token}, ${literal})`);
}
write(file, css);
// Unmapped literals are left in place — they are step 2's
// remaining work, not noise to silence mechanically.
}Note the shape of the replacement: the literal survives as the fallback, so behavior cannot change in the same commit that structure does. Boring, staged, revertible—that is the whole strategy.
Verification Checklist
Additional Resources
Where to go next, inside and outside this system:
- Design Tokens — the token architecture this page's color layer sits inside: formats, resolution, and validation (
/blueprints/foundations/tokens). - Core vs Semantic layers — a worked deep-dive on the layer boundary this page applies to color (
/blueprints/foundations/tokens/core-vs-semantic). - WCAG 2.1 contrast requirements — the normative definitions behind the 4.5:1 / 3:1 / 7:1 thresholds (
https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html). - DTCG design tokens format — the W3C community group format the token sources follow (
https://tr.designtokens.org/format/). - Adaptive-DS-Colors — the generator behind the ramps: contrast-keyed at 1.15:1, uniformly distributed in luminance, shared per-level targets across families.
- The sources themselves —
ui/designTokens/core/color.tokens.json,ui/designTokens/semantic/color.tokens.json, and the generatedapp/designTokens.scssare the runnable truth of everything above.
Related Concepts
Related Concepts
Reflection Questions
Apply This Concept
Your team wants a "celebration" color for success banners. Walk through the five steps of shipping it: which ramp, which semantic group, what the paths extensions declare, what the validator must check, and how a banner component consumes it.
Reflect
A designer argues for hardcoding the flagship brand hex in the hero component because "the ramp is close enough." What does the system lose, and which escape hatch preserves both the brand value and the architecture?
Reflect
The accent passes 4.9:1 on white but a review wants accent-colored body text. What threshold applies, what is the risk, and what does a system-correct resolution look like?
