StartFoundationsComponentsContent

Color

Pick a color by its role, not its value. Every color token follows the theme and the primary color, so the same names work everywhere.

GuideTokens

Principles

  • Neutral first. Surfaces, text, and borders are gray and white. Color is rare, so where it appears it means something: the main action, a status, a category.
  • Choose by role. Use a role token such as text/default or fill/accent. The raw palette (gray-100, blue-500) doesn't follow the theme.
  • Themes and primary colors change values, not names. The same token works in light, dark, and high contrast, and with every primary color.
  • Never rely on color alone. Pair a status, a selection, or an error with an icon, text, or shape.

Token names

A color token reads as role / intent / level / state. Parts that don't apply are left out.

  • Role: surface, text, icon, border, fill, state.
  • Intent: accent, success, warning, error, info. Neutral has no intent in the name: text/default, not text/neutral/default.
  • Level: for example secondary, subtle, strong, raised.
  • State: for example hover, active, disabled, on-fill.

In CSS, replace the slashes with hyphens:

Token CSS variable
text/default var(--text-default)
text/secondary var(--text-secondary)
fill/accent/hover var(--fill-accent-hover)
surface/error var(--surface-error)
text/accent/on-fill var(--text-accent-on-fill)
.card {
  background: var(--surface-default);
  color: var(--text-default);
  border: 1px solid var(--border-default);
}

Themes

  • Light: a neutral frame on white. Depth comes from gray steps.
  • Dark: its own calibration, not inverted light. Higher layers are lighter: the page is darkest, floating panels lightest.
  • High contrast: stronger text, borders, and fills, and hover, press, and selected look different from each other. It meets WCAG contrast throughout.

Choosing a theme

Set data-theme on <html> to light, dark, or high-contrast.

<html data-theme="dark">

Without the attribute, the page is light, unless the operating system asks for more contrast: then it gets high contrast automatically. A value you set always wins.

data-theme OS asks for more contrast OS says nothing
not set high contrast light
light light light
dark dark dark
high-contrast high contrast high contrast

data-theme also works on part of a page:

Publish Save draft
<div data-theme="dark" style="background: var(--surface-page); color: var(--text-default); padding: var(--space-16)">
  <tk-button variant="primary">Publish</tk-button>
  <tk-button>Save draft</tk-button>
</div>

Forced colors mode (Windows High Contrast) is separate from these themes: the operating system replaces the colors with the user's own.

Primary color and accent

The accent (the primary button, links, checked controls, the focus ring) takes its hue from one primary color. Set data-primary on <html>, next to data-theme:

<html data-theme="light" data-primary="purple">

Values: blue (the default), orange, green, mint, teal, indigo, purple, brown, gray, black.

Publish Preview Email me updates
<tk-button variant="primary">Publish</tk-button>
<tk-button variant="ghost">Preview</tk-button>
<tk-checkbox checked>Email me updates</tk-checkbox>
  • black is a monochrome accent: a black primary button with a white label in light and high contrast, a white one with a black label in dark.
  • The label on an accent fill (text/accent/on-fill) is white for every primary color except orange, where it is black.
  • info is not the accent. info is always blue; the accent follows the primary color. Don't swap them, even when the primary color is blue.

The numbers are weights of the primary color's ramp in light, dark, and high contrast.

Token Use Light Dark High contrast
fill/accent primary button, checked checkbox, radio and switch, slider 500 500 600
fill/accent/hover hover on an accent fill 600 400 700
fill/accent/active press on an accent fill 600 400 800
fill/accent/disabled not used: disabled controls take surface/disabled 200 800 300
text/accent links, ghost button labels 600 400 800
text/accent/hover link and ghost label hover 800 200 900
text/accent/on-fill text on fill/accent white or black white or black white or black
icon/accent accent icons 600 400 700
icon/accent/on-fill icons on fill/accent as text/accent/on-fill
border/accent an accent edge, such as a resize handle while dragging 500 500 700
border/accent/focus the focus ring and a focused field's border 500 500 800
state/accent/hover accent hover layer 600 at 10% 500 at 10% 800 at 18%
state/accent/press accent press layer 600 at 10% 500 at 10% 800 at 46%
state/accent/on accent selected layer 600 at 10% 500 at 10% 800 at 46%

In light and dark, fill/accent/active equals fill/accent/hover, so a primary button has no separate press look there. The focus ring has its own page: Focus.

Roles

Role What it colors
surface backgrounds: the page, cards, panels, popovers
text anything people read
icon icons that stand on their own
border outlines, dividers, selection
fill small controls with their own fill, such as a switch track or a solid badge
state translucent hover, press, and selected layers over any of the above

Surface

Token Use Light Dark High contrast
surface/page the page canvas white gray-950 gray-50
surface/default content blocks and cards white gray-900 gray-50
surface/subtle a slightly recessed area inside a block gray-50 gray-800 gray-100
surface/raised floating panels: menus, popovers, dialogs white gray-900 gray-50
surface/sunken wells and tracks, such as a progress track gray-100 gray-950 gray-200
surface/inverse a panel in the opposite tone: tooltip, toast gray-900 gray-50 gray-950
surface/disabled the fill of a disabled control gray-50 gray-900 gray-100
surface/backdrop the dimmed page behind a modal dialog, nothing else gray-950 at 50% black at 60% gray-950 at 60%

Surfaces have no hover or selected tokens: use the state layers below.

Text

Token Use Light Dark High contrast
text/default headings and body gray-950 gray-50 gray-950
text/secondary captions, metadata gray-700 gray-300 gray-900
text/tertiary timestamps, counters gray-600 gray-400 gray-800
text/placeholder placeholder text in an empty field gray-500 gray-500 gray-600
text/disabled text of a disabled control gray-400 gray-600 gray-500
text/inverse text on surface/inverse gray-50 gray-950 gray-50
text/on-fill text on fill/default gray-950 white gray-950

Icon

Each icon/* token pairs with the text/* token of the same level. Icons need 3:1 rather than 4.5:1, so in light and high contrast an icon is one step lighter than its text; in dark it matches its text.

Token Light Dark High contrast
icon/default gray-900 gray-50 gray-900
icon/secondary gray-600 gray-300 gray-800
icon/tertiary gray-500 gray-400 gray-700
icon/disabled gray-300 gray-600 gray-400
icon/inverse gray-50 gray-950 gray-50
icon/on-fill as text/on-fill

Border

Token Use Light Dark High contrast
border/subtle dividers inside a block; the border of fields and outline buttons gray-100 gray-700 gray-600
border/default outlines of containers and controls gray-200 gray-600 gray-700
border/strong edges that must stand out, such as a split button's divider gray-300 gray-400 gray-800
border/selected a selected item gray-800 gray-200 gray-900
border/inverse a border or focus ring on surface/inverse gray-50 gray-950 gray-50
border/disabled the border of a disabled control gray-100 gray-900 gray-300

The focus ring is border/accent/focus, in Primary color and accent.

Fill

Token Use Light Dark High contrast
fill/default a neutral control fill, such as an off switch track gray-100 gray-800 gray-200
fill/hover its hover gray-200 gray-700 gray-300
fill/active its press gray-300 gray-600 gray-400
fill/selected a selected neutral fill gray-300 gray-600 gray-400
fill/disabled a disabled fill gray-100 gray-900 gray-200

Text and icons on a fill take its on-fill partner: text/on-fill on fill/default, text/accent/on-fill on fill/accent, text/error/on-fill on fill/error.

State layers

Hover, press, and selected are translucent layers drawn over whatever the element already shows. Paint them with background-image and keep the element's own background-color. Each layer has a ready gradient: --state-layer-hover, --state-layer-press, --state-layer-on, --state-layer-on-hover, the accent ones (--state-layer-accent-hover, -press, -on), and the error ones (--state-layer-error-hover, -press).

.row:hover { background-image: var(--state-layer-hover); }
.row[aria-selected='true'] { background-image: var(--state-layer-on); }

Don't set the background shorthand on that element: it resets background-image and removes the layer.

Token Use Light Dark High contrast
state/hover under the pointer gray-950 at 5% gray-50 at 5% gray-950 at 13%
state/press while pressed gray-950 at 5% gray-50 at 5% gray-950 at 26%
state/on selected, pressed on, or current gray-950 at 5% gray-50 at 5% gray-950 at 38%
state/on-hover an on item under the pointer gray-950 at 9.75% gray-50 at 9.75% gray-950 at 38%

In light and dark, hover, press, and on are the same tint. Mark a selected item with something more: border/selected, a check, or a bolder label. High contrast tells them apart.

Status colors

Statuses keep their hue whatever the primary color: success is green, warning orange, error red, info blue.

Payment received. Your trial ends in 3 days. The upload failed. Try again. Reports update every hour.
<tk-section-message tone="success">Payment received.</tk-section-message>
<tk-section-message tone="warning">Your trial ends in 3 days.</tk-section-message>
<tk-section-message tone="error">The upload failed. Try again.</tk-section-message>
<tk-section-message tone="info">Reports update every hour.</tk-section-message>
Paid Due soon Overdue New
<tk-badge tone="success">Paid</tk-badge>
<tk-badge tone="warning">Due soon</tk-badge>
<tk-badge tone="error" treatment="solid">Overdue</tk-badge>
<tk-badge tone="info" treatment="solid">New</tk-badge>

Each cell gives the weight of the status hue in light · dark · high contrast.

Token success (green) warning (orange) error (red) info (blue)
surface/{status}: a tinted background 50 · 900 · 100 50 · 900 · 100 50 · 900 · 100 50 · 900 · 100
text/{status} 600 · 400 · 800 600 · 400 · 800 600 · 400 · 800 600 · 400 · 800
icon/{status} 600 · 400 · 700 600 · 400 · 700 600 · 400 · 700 600 · 400 · 700
border/{status} 300 · 500 · 600 300 · 500 · 600 600 · 400 · 600 300 · 500 · 600
fill/{status}: a solid fill 600 · 500 · 700 500 · 500 · 700 600 · 500 · 700 600 · 600 · 700
text/{status}/on-fill, icon/{status}/on-fill white black · black · white white white

error also has interaction tokens:

Token Use Light Dark High contrast
state/error/hover hover on a danger button or menu item red-600 at 10% red-500 at 10% red-800 at 18%
state/error/press press on the same; equals hover red-600 at 10% red-500 at 10% red-800 at 18%
fill/error/hover hover on a solid red fill; no component uses it red-700 red-600 red-800
fill/error/active press on a solid red fill; no component uses it red-800 red-700 red-900

Categorical palette

Thirteen colors tell categories apart: tags, labels, chart series, avatars. They carry no status: a red tag is not an error.

Research Design Marketing Sales Unassigned
<tk-tag color="purple">Research</tk-tag>
<tk-tag color="teal">Design</tk-tag>
<tk-tag color="orange">Marketing</tk-tag>
<tk-tag color="green">Sales</tk-tag>
<tk-tag color="gray">Unassigned</tk-tag>

The colors: red, orange, yellow, green, mint, teal, cyan, blue, indigo, purple, pink, brown, gray. Each has three tokens; the numbers are weights of that color in light · dark · high contrast.

Token CSS variable Use Weights
categorical/{color}/surface --{color}-surface the label background 100 · 900 · 100
categorical/{color}/text --{color}-text the label text 700 · 300 · 900; 800 in light for orange, yellow, green, mint, teal, cyan
categorical/{color}/border --{color}-border an optional outline 400 · 500 · 700
  • The text on its surface is at least 5:1 in every theme.
  • The border is decorative: under 3:1 in light. Don't let it carry meaning.
  • Keep to five or six categorical colors on one screen; more are hard to tell apart.
  • Take a chart's axes and labels from the neutral tokens.

Syntax palette

Code block colors code with the syntax/* tokens. Like the categorical palette, they carry no status: a string is green, but it isn't success.

// Show a price in dollars
function formatPrice(amount) {
  return '$' + amount.toFixed(2);
}
<tk-code-block language="js"><pre>// Show a price in dollars
function formatPrice(amount) {
  return '$' + amount.toFixed(2);
}</pre></tk-code-block>
Token Light Dark High contrast
syntax/surface gray-50 gray-950 white
syntax/keyword purple-700 purple-300 purple-800
syntax/string green-700 green-300 green-800
syntax/comment gray-700 gray-400 gray-700
syntax/number orange-700 orange-300 orange-800
syntax/function blue-700 blue-300 blue-800
syntax/class teal-700 teal-300 teal-800
syntax/variable red-700 red-300 red-800
syntax/punctuation gray-800 gray-300 gray-800

Every syntax color is at least 4.5:1 on syntax/surface in every theme.

Pairing

Take a background and its partner tokens together, and the contrast is built in.

Recipe Background Text Icon Border
Card surface/default text/default icon/default border/default
Selected row its surface + the state/on layer text/default icon/default border/selected or a check
Neutral fill fill/default text/on-fill icon/on-fill none
Primary button fill/accent text/accent/on-fill icon/accent/on-fill none
Link any surface text/accent none none
Status message surface/{status} text/{status} icon/{status} border/{status}
Solid badge fill/{status} text/{status}/on-fill icon/{status}/on-fill none
Tooltip, toast surface/inverse text/inverse icon/inverse border/inverse
Disabled control surface/disabled text/disabled icon/disabled border/disabled
Tag categorical/{color}/surface categorical/{color}/text none categorical/{color}/border, optional

Accessibility

Built in:

  • Every background role has partner text and icon tokens; the pairs above meet their contrast targets, except the limits below.
  • High contrast meets WCAG contrast throughout, and people who ask their operating system for more contrast get it automatically.
  • Disabled looks come from their own tokens, not opacity.
  • Placeholder and disabled text sit under 4.5:1 on purpose: they aren't content.

Your part:

  • Don't carry meaning by color alone; add an icon, text, or shape.
  • Check any pair not on this page: 4.5:1 for text; 3:1 for large text (24px, or 18.66px bold), icons, and borders that carry meaning.
  • Use text/* for text and icon/* for icons. Icon colors only need 3:1 and can be too light for text.

Measured pairs

Light theme, blue primary.

Background Foreground Ratio
surface/default text/default 17.36:1
surface/default text/secondary 6.66:1
surface/default text/tertiary 4.59:1
surface/default text/accent 5.49:1
surface/error text/error 4.63:1
surface/info text/info 5.05:1
fill/default text/on-fill 13.06:1
fill/accent text/accent/on-fill 4.02:1
fill/success text/success/on-fill 4.66:1
fill/warning text/warning/on-fill 9.55:1
fill/error text/error/on-fill 5.07:1
fill/info text/info/on-fill 5.49:1

Contrast limits

These fall short in light or dark. High contrast meets the minimums.

  • Status text on its tint: text/success 4.29:1 and text/warning 3.02:1 in light, under 4.5:1. text/warning is 3.30:1 even on white.
  • Accent text: text/accent is 3.30:1 on white with the orange primary. In dark, on surface/subtle and surface/raised, it is under 4.5:1 with every primary color except orange and black; indigo is lowest, at 2.47:1.
  • Label on the accent fill: at least 3:1 with every primary color (lowest gray, 3.26:1), but under 4.5:1 with several, such as blue at 4.02:1.
  • Field and outline-button border (border/subtle): 1.33:1 in light and 2.00:1 in dark, under 3:1; 4.22:1 in high contrast. In light it disappears on surface/sunken.
  • border/strong: 2.02:1 on white in light, under 3:1.
  • Danger button label on hover and press: 3.94:1 in light.
  • Focus ring: under 3:1 with some primary colors on some surfaces. See Focus.
  • Selected and hovered look the same in light and dark. Mark selection with a border, a check, or a bolder label.

Do / Don't

✅ Do ❌ Don't
Put text/accent/on-fill on fill/accent Put text/default on an accent fill
Keep fill/* to small controls Use a fill as a page or panel background
Show a disabled control with the */disabled tokens Dim it with opacity
Take status colors from success, warning, error, info Use the accent for a status
Tell labels apart with a categorical color Use a red tag to signal an error
Mark a selected item with border/selected or a check Rely on the state/on tint alone
Paint state layers with background-image Set background on an element with a state layer
Use the same token in every theme Put the theme in a name, like surface-default-dark

Primitives

Under the roles sits the palette: 13 hues (red, orange, yellow, green, mint, teal, cyan, blue, indigo, purple, pink, brown, gray), each in 11 weights from 50 to 950, plus white and black. In CSS: var(--blue-500).

Primitives don't change with the theme or the primary color, so use a role token wherever one fits. The same weight looks about equally heavy in every hue, but contrast still differs: orange and yellow are lighter than blue or gray at the same weight.

Every value, with its contrast against white and black: Primitive colors.

  • Focus: the focus ring and its color.
  • Elevation: shadows that lift raised surfaces.
  • Badge, Section message, and Banner: the status colors in use.
  • Tag: the categorical palette in use.