Design system · colour assignment
Give every stylable asset a considered colour — and prove it passes.
Work top to bottom. Each row is one asset or role you can style with CSS3: note the palette token you assign, tick it when done, and confirm it meets the contrast gate. The order builds the brand from foundations up to how it behaves live — static surfaces first, then every interaction and user state, then the WCAG 2.2 gates that keep it accessible. Ticks and tokens save in this browser.
Contrast gates (WCAG 2.2)
- 4.5:1 Body text vs its background 1.4.3 AA
- 3:1 Large text (≥24px, or ≥18.66px bold) 1.4.3
- 3:1 UI boundaries, icons, focus rings, chart marks 1.4.11
- 7:1 Body target where practical 1.4.6 AAA
- exempt Disabled controls & pure decoration — keep legible anyway
The 10-state grammar
- Every interactive asset needs a colour for default, hover, focus-visible, active, loading, success, error, disabled, plus empty/filled and selected/current.
- Never colour alone (1.4.1): pair every state with a second cue — underline, icon, weight, border, dot or text.
01
Palette foundations — define your roles first
0/12
Before touching a component, name the roles. Everything downstream references these tokens, never a raw hex. Assign one value per role for each theme (dark and light).
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Page background--bg | background of <body> / the app canvas |
The ground every text ratio is measured against. Pick this first. | ||
| Surface--surface | background of cards, panels, nav, inputs |
3:1 boundary vs page if borderless (1.4.11). | ||
| Raised / recessed--bg-alt | elevated menus, wells, table zebra | Distinguish by lightness step, not hue alone. | ||
| Text — primary--text | body copy, headings | 4.5:1 vs every surface it sits on. Aim 7:1. | ||
| Text — muted--text-muted | captions, helper, metadata | Still 4.5:1 (or 3:1 if large). Muted ≠ low-contrast. | ||
| Brand — primary (copper)--color-primary | logo, emphasis, primary marks | Text on it 4.5:1; the mark 3:1 vs page. | ||
| Brand — secondary / accent--color-accent | active states, focus, links, CTAs | Accent shape 3:1 vs page; on-accent text 4.5:1. | ||
| Border / divider--surface-border | hairlines, input borders, dividers | Structural borders that group: 3:1 (1.4.11). | ||
| Focus ring--color-accent | the visible keyboard focus indicator | 3:1 vs adjacent; ≥2px; 2.4.7 + 2.4.13. | ||
| Semantic — success / warning / danger(page-scoped) | status text, badges, alerts, validation | Matched saturation; usable as text (4.5:1) and mark (3:1). | ||
| Scrim / overlay--scrim | the dim behind modals & drawers | Enough dim to separate layers; no text ratio required. | ||
| Selection::selection | highlighted text colour + background | Selected text ≥4.5:1 against the selection background. |
02
Global & structural surfaces
0/8
The static skeleton — the colours a page wears before anything is interacted with.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Body / canvas--bg | background, base color, color-scheme |
Set color-scheme so controls & scrollbars theme correctly. |
||
| Section bands | alternating background between sections |
Separate by lightness; add a hairline if 3:1 not met. | ||
| Containers / cards--surface | background, border, box-shadow |
Shadow is not a contrast substitute — borderless cards still need 3:1. | ||
| Dividers / rules--surface-border | border-color, hr |
Decorative: any contrast. Grouping: 3:1. | ||
| Scrollbars | scrollbar-color, ::-webkit-scrollbar* |
Track/thumb visible in both themes; follow color-scheme. |
||
| Text selection::selection | background + color |
Legible highlight; test on dark and light grounds. | ||
| Skip-to-content link | background, color on focus |
Visible on focus; 4.5:1; first focusable element. | ||
| Ambient background | decorative gradients, body::before |
Decorative only — must not drop text below its ratio. |
03
Typography & text colour
0/9
Colour applied to words. Each is measured against the surface directly behind it.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Headings h1–h6--text | color |
4.5:1 (large sizes may use 3:1, but do not lean on it). | ||
| Body copy--text | color |
4.5:1 minimum; 7:1 target for long-form. | ||
| Secondary / lede--text-muted | color |
4.5:1 — the commonest failure point. Verify, don’t eyeball. | ||
| Links (in body)--color-accent | color, text-decoration-color |
3:1 vs surrounding text and a non-colour cue (underline). | ||
| Link states--color-accent | hover / visited / focus / active | Each distinct; visited still passes; focus ring 3:1. | ||
| Inline code / kbd | color, background, border |
Code text 4.5:1 vs its tinted background, not the page. | ||
| Blockquote--text-muted | color, left border-color |
Accent bar 3:1 if it carries meaning. | ||
| List markers::marker | color |
Decorative exempt; if they encode state, 3:1. | ||
| Captions / metadata--text-muted | color, tabular-nums |
Small text still needs 4.5:1. |
04
Buttons & calls to action
0/9
Three variants, each across the full state grammar. Colour is never the sole affordance — shape, border and label carry it too.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Primary--color-accent / --gradient-primary | background, color, border |
On-accent label 4.5:1; fill 3:1 vs page. | ||
| Secondary--surface-border | border-color, color, background |
Border 3:1 vs page; label 4.5:1. | ||
| Ghost / tertiary | color, hover background |
Needs a visible hover/focus (no resting border). | ||
| Destructive--cl-bad | outline or fill | Red is not the only cue — add wording/icon (1.4.1). | ||
| Hover:hover | background/border shift |
Perceivable; keep contrast — don’t lighten to failure. | ||
| Focus-visible:focus-visible | box-shadow/outline ring |
3:1 ring, ≥2px, 3px offset; never remove without replacing. | ||
| Active / pressed:active | background, subtle transform |
Distinct from hover; instant (~80ms). | ||
| Loading[aria-busy] | spinner color, disabled look |
Announce busy; keep label legible. | ||
| Disabled:disabled | muted background/color |
Contrast-exempt, but readable and clearly inert. |
05
Forms, inputs & validation
0/12
Where user behaviour meets colour most intensely. Every field state needs a colour and a non-colour signal.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Label--text | color |
4.5:1; a real <label>, never placeholder-as-label. |
||
| Input surface--surface | background, border-color, color |
Border 3:1 vs page; typed text 4.5:1 vs field. | ||
| Placeholder:placeholder-shown | ::placeholder color |
Needs 4.5:1; never carries essential info. | ||
| Helper text--text-muted | color |
4.5:1; describe format before the user errs. | ||
| Focus ring:focus-visible | border-color, box-shadow |
3:1; the ink-stroke / ring must be obvious. | ||
| Valid:user-valid | border-color, ✓ icon |
Green + tick, not green alone. | ||
| Error:user-invalid | border-color, message color |
Red border + text via aria-describedby (1.4.1, 3.3.1). |
||
| Checkbox / radio:checked | box background/border, tick |
Checked mark 3:1; not conveyed by fill colour alone. | ||
| Toggle / switch:checked | track + thumb background |
Distinguishable to colour-blind users — add position/label. | ||
| Select / dropdown | control + option color/background |
Caret 3:1; open list follows theme. | ||
| Range / slider | track, filled range, thumb | Filled 3:1 vs track; thumb 3:1 vs both. | ||
| Disabled / readonly | muted background/color |
Clearly inert; contrast-exempt but readable. |
07
Tables, lists & dense data
0/8
High-density surfaces where zebra, hover and selection colours must stay subtle yet perceivable.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Table header--bg-alt | background, color, bottom border |
Header text 4.5:1; sort affordance not colour-only. | ||
| Row / cell | color, cell border |
Body 4.5:1; grouping borders 3:1. | ||
| Zebra striping:nth-child | alternating row background |
Lightness step; must not drop text contrast. | ||
| Row hover:hover | row background |
Perceivable but calm; text stays legible over it. | ||
| Selected row[aria-selected] | background + indicator |
Tint + a check/edge, not tint alone. | ||
| Sortable headeraria-sort | arrow color, active column |
Direction shown by icon + text, 3:1. | ||
| Sticky header--surface | background stays opaque on scroll |
Opaque so rows don’t bleed through. | ||
| Empty / no-data--text-muted | placeholder color |
4.5:1 guidance; explain the next action. |
08
Badges, pills, tags & chips
0/5
Small status objects that lean hardest on colour — so they most need a second signal.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Status pillsemantic | background (tint), color, border |
Text on tint 4.5:1; add a dot + word. | ||
| Neutral tag--surface-border | background, color, border |
4.5:1; consistent radius with the system. | ||
| Removable chip | close-icon color, hover |
Close control 3:1 + ≥24px target (2.5.8). | ||
| Count / notification badge--cl-bad | background, color |
On-badge number 4.5:1; not the sole notifier. | ||
| Category dot | background of the dot |
If it encodes category, pair with a label. |
09
Feedback, notifications & loading
0/7
Transient and persistent messaging — one visual grammar, four severities, always with words.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Toastsemantic | background, color, icon, border |
Severity by icon + text + colour; content 4.5:1. | ||
| Inline alert / bannersemantic | tinted background, border, color |
Icon + heading carry meaning alongside the tint. | ||
| Form status linesemantic | color of the message |
Never a bare colour change — always a sentence. | ||
| Progress bar--color-accent | track + fill background |
Fill 3:1 vs track; pair with a % or label. | ||
| Spinner / loader--color-accent | color, border-color |
Announce busy; visible on the surface. | ||
| Skeleton--bg-alt | placeholder background, shimmer |
Subtle; no shimmer under reduced-motion. | ||
| Empty state--text-muted | illustration + guidance color |
Guidance 4.5:1; give a next step. |
10
Overlays — modals, popovers, tooltips
0/5
Layered surfaces above the page. Each needs its own opaque background and to read against the scrim.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Modal / dialog--surface | background, border, box-shadow |
Opaque surface; content passes; trap focus. | ||
| Backdrop / scrim--scrim | background (dim) |
Separates layer; dialog title 4.5:1 on its surface. | ||
| Popover--surface | background, border, arrow |
Edge visible vs page (3:1); actionable content passes. | ||
| Tooltip--bg-alt | background, color |
4.5:1; also satisfy 1.4.13 (dismissable, hoverable). | ||
| Context menu--surface | item color, hover, separators |
Hover 3:1; destructive item + word, not red alone. |
11
Media, icons & graphics
0/5
Non-text visuals still carry contrast duties when they convey meaning.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Icons — meaningful--text / --color-accent | color / fill |
3:1 vs background when they convey info (1.4.11). | ||
| Icons — decorative--text-muted | color |
Exempt; mark aria-hidden. |
||
| Avatars / initials--color-primary | background, initial color |
Initial 4.5:1 on the generated background. | ||
| Image overlays / captions | gradient/scrim under text, caption color |
Text-over-image needs a scrim to hold 4.5:1 everywhere. | ||
| Logo / wordmarkbrand | on-light and on-dark variants | Provide both; holds on every surface it lands on. |
12
Data visualisation
0/7
Charts have their own rules — marks against ground, marks against each other, and never hue as the only channel.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Categorical seriesviz-1…n | series fill/stroke |
Each mark 3:1 vs plot ground; distinguishable by shape/label too. | ||
| Sequential / diverging | ramp stops | Perceptually even; check colour-blind safety. | ||
| Axes & gridlines--surface-border | stroke, tick color |
Gridlines may be <3:1 (decorative); axis labels 4.5:1. | ||
| Data labels / values--text | color |
4.5:1 wherever they sit (over bars too). | ||
| Tooltip / crosshair--surface | background, color |
Readable over any series. | ||
| Legend | swatch + label | Swatch 3:1; label 4.5:1; not optional for colour coding. | ||
| Thresholds / statessemantic | good / warn / critical bands | Encode with label or pattern in addition to colour. |
13
Interaction-state matrix
0/12
The colour every interactive element must define — and the second, non-colour cue that rides alongside it (WCAG 1.4.1). Assign the token; the cue column is the rule.
| ✓ | State | Suggested | Assign token | Required 2nd cue | Note |
|---|---|---|---|---|---|
| Default:link / resting | base token | — | Resting colour of the element. | ||
| Hover:hover | accent / tint shift | background / underline appears | Pointer only — never the sole way to reach anything. | ||
| Focus-visible:focus-visible | --color-accent ring | 2px ring, 3px offset | 3:1 (2.4.7 / 2.4.13). The keyboard user’s lifeline. | ||
| Active / pressed:active | darker / inset | transform / inset shadow | Instant feedback (~80ms). | ||
| Selected / current[aria-current] / [aria-selected] | --color-accent | underline / check / bar | Programmatic + visual; not colour alone. | ||
| Loading[aria-busy] | muted + spinner | spinner / progress | Announce; block re-entry. | ||
| Success— | --cl-ok | icon ✓ + text | Green + tick + words. | ||
| Error— | --cl-bad | icon + message | Red + text via aria-describedby. | ||
| Disabled:disabled | muted | reduced opacity + cursor | Contrast-exempt; still clearly inert. | ||
| Empty / filled:placeholder-shown | muted vs primary | placeholder vs value | Distinguish an empty field from a filled one. | ||
| Expanded[aria-expanded] | --color-accent | caret rotation | State on the trigger, mirrored in colour + icon. | ||
| Drag / drop.dragover | accent tint | dashed edge / insert line | Drop target obvious without colour alone. |
14
User & environment preferences
0/7
Colour that must adapt to the person and device — the difference between a brand and an accessible brand.
| ✓ | Asset / role | Stylable with CSS3 | Assign token | Contrast / WCAG |
|---|---|---|---|---|
| Light / dark parityprefers-color-scheme | full token set per theme | Re-run every contrast check in both themes — don’t assume inversion works. | ||
| Explicit theme toggle[data-theme] | override tokens both directions | Toggle must beat the OS setting in both directions. | ||
| Increased contrastprefers-contrast: more | stronger borders, less tint | Raise border/text contrast; reduce translucency. | ||
| Forced coloursforced-colors | system palette + SystemColors |
Windows High Contrast — keep focus + borders, don’t fight it. | ||
| Reduced motionprefers-reduced-motion | static state changes | Colour shift replaces animated transitions. | ||
| Print@media print | ink-on-white, remove chrome | Text legible without the brand background. | ||
| Colour-blind safety | deuter / prot / trit review | No pair relies on red↔green alone; verify with a simulator. |
15
WCAG & contrast sign-off
0/12
The gates every colour decision above must clear. Tick each once verified with a tool, not by eye.
| ✓ | Criterion | Requirement | How to verify |
|---|---|---|---|
| 1.4.3 Text contrast | Body ≥ 4.5:1 · large ≥ 3:1 | Check every text/surface pair with a contrast checker. | |
| 1.4.6 Enhanced (AAA) | Body ≥ 7:1 where practical | Target for long-form reading surfaces. | |
| 1.4.11 Non-text contrast | UI parts, focus, icons, chart marks ≥ 3:1 | Borders, states, meaningful graphics. | |
| 1.4.1 Use of colour | Never the only signal | Every state also has icon / text / shape / underline. | |
| 2.4.7 Focus visible | Visible keyboard focus on all controls | Tab the whole page; nothing invisible. | |
| 2.4.11 / 2.4.13 Focus (2.2) | Not obscured · adequate appearance | Ring not hidden by sticky bars; ≥2px, 3:1. | |
| 2.5.8 Target size (2.2) | Interactive targets ≥ 24×24px | Chip close buttons, icon buttons, table actions. | |
| 1.4.10 Reflow | Usable at 320px, no 2-D scroll | Colour bands must not force horizontal scroll. | |
| 1.4.4 Resize text | Legible at 200% zoom | No fixed-px colour boxes that clip enlarged text. | |
| 2.3.3 Animation from interactions | Honour reduced-motion | Colour transitions become instant. | |
| Both themes verified | Light + dark both pass every gate | Dark mode is where ratios silently fail. | |
| Colour-blind pass | No red/green-only distinctions | Simulate deuteranopia, protanopia, tritanopia. |