Skip to content

Colors

Six ramps, five semantic roles, and one rule that outranks all of them: colour is never the only thing carrying the message.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Primitive ramps

Neutral

--p-neutral-*
25
50
100
200
300
400
500
600
700
800
900
950
975

Brand · Iris

--p-brand-*
50
100
200
300
400
500
600
700
800
900
950

Success · Emerald

--p-success-*
50
100
200
300
400
500
600
700
800
900
950

Warning · Amber

--p-warning-*
50
100
200
300
400
500
600
700
800
900
950

Danger · Rose

--p-danger-*
50
100
200
300
400
500
600
700
800
900
950

Info · Blue

--p-info-*
50
100
200
300
400
500
600
700
800
900
950

Semantic roles

Each row is one role in its five certified forms. The -text value is the only one guaranteed to pass on -subtle; the -fg value is the only one guaranteed to pass on the solid base.

--ds-accent
-subtle
-border
-text
-hover
--ds-success
-subtle
-border
-text
-hover
--ds-warning
-subtle
-border
-text
-hover
--ds-danger
-subtle
-border
-text
-hover
--ds-info
-subtle
-border
-text
-hover

Live contrast audit

Computed from the running stylesheet on every render, with alpha tints composited against their surface. Switch the app theme and watch every row recalculate.

PairSampleRatioGrade
Body text on pageAa 15px——
Secondary text on cardAa 15px——
Caption on cardAa 15px——
Label on filled buttonAa 15px——
Accent text on tonal fillAa 15px——
Success text on tintAa 15px——
Warning text on tintAa 15px——
Danger text on tintAa 15px——
Label on danger buttonAa 15px——
Disabled text (exempt)Aa 15px—exempt

The greyscale test

Run any status UI through a greyscale filter. If you can still tell success from failure, colour was doing its job as reinforcement. If you cannot, colour was doing the whole job.

Full colour

Deployed

Build 4021 is live in three regions.
HealthyDegradedDown
Greyscale

Deployed

Build 4021 is live in three regions.
HealthyDegradedDown

Categorical palette

Eight hues ordered for maximum separation between neighbours, including under deuteranopia. Never used for status — a chart series that happens to be red does not mean an error.

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

Neutral
Default
Accent
Accent
Success
Success
Warning
Warning
Danger
Danger
Info
Info
Critical
SolidUses the -fg pair
Danger
GreyscaleStill legible

Anatomy

Every part, every measurement, and the reason it is that number.

Payment declined

Card ending 4242 was refused by the issuer.

1 · fill

--ds-danger-subtle

2 · border

--ds-danger-border

3 · icon+label

--ds-danger-text

4 · body

--ds-fg-secondary

One status message uses four different tokens from two families. Substituting any one of them for another breaks either contrast or meaning.

  1. Subtle fill14% alpha of the 400 step

    Alpha, not a solid, so the same token works over a card, a table row, or a nested panel. A solid tint only matches one background.

  2. Border34% alpha

    Must reach 3:1 against the surrounding surface — WCAG 1.4.11 covers UI boundaries, not just text.

  3. Status text and icon--ds-danger-text

    A lighter ramp step than the base in dark mode and a darker one in light mode. This is the pair certified against the subtle fill.

  4. Body copy--ds-fg-secondary

    Deliberately neutral. Colouring the entire message red reduces legibility and makes the severity read as louder than it is.

  5. Redundant encodingicon + heading text

    The word "declined" and the triangle both carry the meaning. Remove the colour entirely and nothing is lost.

Color palette

Every colour in the active theme, resolved from the running stylesheet. Click any swatch to copy its value.

Primitives

--p-*Raw values. Identical in both themes — never reference these from a component.

Neutral

--p-neutral-*

Brand · Iris

--p-brand-*

Success · Emerald

--p-success-*

Warning · Amber

--p-warning-*

Danger · Rose

--p-danger-*

Info · Blue

--p-info-*

Categorical

--p-viz-*

Semantics

--ds-*What components actually use. Switch the theme and every value below re-reads.

Surfaces

Interaction layers

Foreground

Borders

Brand & focus

Success

Warning

Danger

Info

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
Surfaces
--ds-canvas—Page background — the furthest-back plane
--ds-sunken—Recessed regions that read as below the canvas
--ds-surface—Cards, panels, the default raised plane
--ds-surface-raised—A surface on a surface
--ds-surface-overlay—Dialogs, menus, popovers
--ds-surface-inset—Wells — inputs, code, table headers
Interaction layers
--ds-layer-hover—Hover wash over any surface
--ds-layer-active—Pressed and held states
--ds-layer-selected—Persistent selection tint — never reuse the hover value
--ds-layer-scrim—Backdrop behind modals and drawers
Foreground
--ds-fg—Headings and primary text
--ds-fg-secondary—Body copy
--ds-fg-muted—Captions, metadata, placeholders, icons
--ds-fg-disabled—Disabled text — formally exempt from contrast rules
--ds-fg-on-accent—Text sitting on a solid brand fill
--ds-fg-inverse—Text on an inverted surface — tooltips, toasts
Borders
--ds-border-subtle—Dividers and card edges
--ds-border—Default component borders
--ds-border-strong—Checkbox and radio outlines, scrollbar thumbs
--ds-border-interactive—The edge of something you can click
Brand & focus
--ds-accent—Primary action and selection
--ds-accent-subtle—Tonal brand fill behind text or an icon
--ds-accent-border—Brand-tinted edge at ≥3:1
--ds-accent-text—The pair certified for text on --ds-accent-subtle
--ds-focus-ring—One ring for the whole system — clears 3:1 on every surface
Status
--ds-success—Completed, healthy, approved
--ds-warning—Degraded, needs attention, reversible risk
--ds-danger—Failed, destructive, irreversible
--ds-info—Neutral system information
--ds-{role}-subtle / -border / -text / -fg—The four certified companions every role ships with
Data visualisation
--p-viz-1 … --p-viz-8—Charts, avatars, categorical tags — never status

Do

HealthyDegradedDown
dot + word + tone
Pair every colour with a second signalIcon, dot, word, or position. The greyscale test above is the check: if the meaning survives, colour is reinforcing. If it does not, colour is load-bearing and the design excludes people.
Card was declined
Use the certified pair for text on a tint--ds-danger-text on --ds-danger-subtle is measured at ≥4.5:1 in both themes. Using the solid --ds-danger there lands around 3:1 and fails for body copy.
overlay
Let the surface ramp carry depthcanvas → surface → raised → overlay is a four-step ladder of lightness. Depth read from surface colour survives greyscale, high-contrast mode, and a phone in direct sunlight; a drop shadow does not.
ProjectsDeploymentsNew project
Reserve the accent for one thing per viewThe brand colour is a pointer. If the nav, the badge, the link, the chart and the button are all accent-coloured, it has stopped pointing at anything and the primary action is camouflaged.

Don't

which one is the error?
Do not signal state with colour aloneThese three rows are identical to anyone with deuteranopia, anyone reading a greyscale export, and anyone whose monitor is badly calibrated. The information simply is not there.

Your trial ends in three days. Add a payment method to keep your projects online.

Do not put solid status colour behind body textStatus colours are tuned to be noticeable, not readable. A paragraph on a solid amber block fails contrast in one theme and is uncomfortable in both.
three purples, one of them real
Do not invent a colour outside the rampsEvery off-ramp hex is a value nobody can theme, nobody can audit for contrast, and nobody can find later. It will be copied into three more components before anyone notices.

Pure #FFF on pure #000 — 21:1 and unpleasant.

Do not use pure black or pure white surfaces in dark modeMaximum contrast causes halation — light text on pure black smears for astigmatic readers, and the eye strains at the boundary. Our canvas is #0A0B0E and our brightest text is #EDEFF4.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.1Use of ColorA1.4.3Contrast (Minimum)AA1.4.6Contrast (Enhanced)AAA1.4.11Non-text ContrastAA

Contrast

  • Body text (under 18.66px regular / 14px bold) needs 4.5:1. Large text needs 3:1. UI boundaries and meaningful graphics need 3:1.
  • Alpha fills must be composited against their actual background before measuring — a 14% tint measured against transparent is meaningless.
  • Disabled controls are formally exempt, which is why "just disable it" is a way of hiding an accessibility problem rather than solving one.
  • AAA (7:1) is the target for long-form reading surfaces. Our body text hits 13.8:1 in dark and 15.9:1 in light.

Keyboard

⌘IInspector Mode reports the live contrast ratio of anything you hover.

Screen readers

  • Screen readers do not announce colour. Any meaning encoded in a fill must also exist as text, an accessible name, or an icon with a label.
  • Charts need direct labels or a data table alternative; a legend that maps colour to series is useless without colour vision.

Focus & touch

  • The focus ring uses a fixed accent step chosen to clear 3:1 against every surface token in both themes — that is why it is --ds-focus-ring rather than simply reusing --ds-accent.
  • Colour has no bearing on hit area, but low-contrast controls are measurably harder to acquire — users aim more slowly at targets they can barely see, which compounds Fitts’ Law.
AttributeApplied toNotes
forced-colors: activeMedia queryWindows High Contrast Mode replaces your palette wholesale. Test it — anything conveyed only by background colour disappears entirely.
color-scheme:rootDeclares dark or light to the UA so native controls, scrollbars, the caret and autofill match your surface.
aria-label / visible textStatus elementsThe state must be readable. A green dot with no accessible name announces as nothing at all.

Code

Example usage

Prefer the semantic utility. Reach for a raw var() only when no utility exists.

tsx
1// Surfaces2<div className="bg-canvas">3  <div className="bg-surface border border-line-subtle rounded-xl">4    <div className="bg-surface-inset" />5  </div>6</div>78// Text hierarchy9<h2 className="text-fg">Primary</h2>10<p  className="text-fg-secondary">Body copy</p>11<p  className="text-fg-muted">Caption</p>1213// A status message — four tokens, two families14<div className="bg-danger-subtle border border-danger-border rounded-lg p-4">15  <AlertTriangle className="text-danger-text" aria-hidden />16  <p className="text-fg">Payment declined</p>17  <p className="text-fg-secondary">Card ending 4242 was refused.</p>18</div>1920// Categorical colour for a chart — never a status family21const SERIES = [1, 2, 3, 4, 5, 6, 7, 8].map(22  (i) => 'var(--p-viz-' + i + ')'23)

CSS

How a status family is defined. Note that both themes are always written together.

css
:root, [data-theme='dark'] {
  --ds-danger:        #ee4351;                    /* solid fill    */
  --ds-danger-hover:  #fa5b68;
  --ds-danger-active: #d62838;
  --ds-danger-fg:     #ffffff;                    /* text ON solid */
  --ds-danger-subtle: rgb(250 100 112 / 0.14);    /* tinted fill   */
  --ds-danger-border: rgb(250 100 112 / 0.36);    /* >= 3:1 edge   */
  --ds-danger-text:   #ff929b;                    /* text ON tint  */
}

[data-theme='light'] {
  --ds-danger:        #d62838;   /* darker: needs 4.5:1 with white  */
  --ds-danger-fg:     #ffffff;
  --ds-danger-subtle: rgb(238 67 81 / 0.10);
  --ds-danger-border: rgb(214 40 56 / 0.28);
  --ds-danger-text:   #b21c2b;   /* darker still: sits on a tint    */
}

/* High-contrast mode strips colour entirely — keep the structure */
@media (forced-colors: active) {
  .status { border: 1px solid; }
}

Notes

Professional tips

  • Design the dark theme first if your product is developer-facing, and the light theme first if it is consumer-facing. Whichever you build second will expose the places where colour was doing structural work.
  • When you need "one more" colour, try changing lightness before changing hue. A second hue costs the user a new thing to learn; a lighter step of an existing hue costs nothing.
  • The accent colour should not appear in your empty states, your table borders, or your body text. If it does, it stops meaning "here is the action".
  • Test on an OLED phone at minimum brightness and on a cheap IPS panel at maximum. Ramps that look smooth on a calibrated monitor often band badly on both.

Performance

  • color-mix() and relative colour syntax are resolved at computed-value time and are effectively free — you do not need to precompute tints into static hexes.
  • Avoid animating background-color on large surfaces during scroll; paint cost scales with area. Animate opacity of an overlay instead.
  • A gradient with many stops is more expensive to paint than a flat fill. On a full-page background, use at most two radial gradients at low alpha.

Common mistakes

  • Measuring contrast against the token value instead of the composited result. A 14% alpha over a dark card is not the colour in the token.
  • Using the same colour for "selected" and "hover". The user cannot tell whether a row is currently selected or just under the cursor.
  • Assigning status colours to chart series. A red line in a revenue chart reads as a failure even when it is just Q3.
  • Reusing the warning amber for a brand highlight. Now every promotional banner in the product looks like a system alert.

Real-world recommendations

  • Ship a colour-blindness simulator in your review process. Chrome DevTools has one built in under Rendering → Emulate vision deficiencies; use it on every status screen before merge.
  • When a stakeholder asks to "make it pop", the answer is almost never more saturation. It is usually more contrast between adjacent elements, or more whitespace around the thing that matters.
  • Keep a single page in your app that renders every semantic pair with its live contrast ratio, exactly like the audit above. It turns an abstract rule into a build-time regression test.
  • Brand colours from a marketing palette are almost never accessible as UI colours. Take the hue, rebuild the ramp for the screen, and give the marketing value back to marketing.

Choosing a role

A decision table for the question that comes up most often.

SituationRoleWhy
The action the product wants you to takeaccentPoints. Exactly one per view.
Operation finished and cannot be undone by mistakesuccessConfirms. Fades from attention quickly.
Something needs attention but nothing is broken yetwarningInterrupts without alarming.
Failed, blocked, or destructivedangerStops. Never used decoratively.
Neutral system fact the user did not ask forinfoInforms without implying action.
A category with no inherent good or badviz-1…8Distinguishes. Carries no valence.