Gesso
Foundations

Color

Choose a utility for brand, chrome, status, and washes, and see which values a look changes.

Color is split into four jobs, and each has its own utilities: brand for identity, chrome for paper and ink, status for meaning, and the alpha scale for washes. This page names the utility for each job and says which values a look changes, so you can pick a color by what it does and not by its hue.

Switch looks in the sidebar to see the split. A look changes the brand colors and the second brand hue. Chrome keeps the same lightness in every look, and a look can add a hue to it, as the warm paper of Washi, ICS, and Ramson does. Without a look the default brand is ink, so a dry install is gray.

Kit markup uses the role names from bindings.css, such as bg-surface and text-fg. The shadcn names stay wired as aliases so a third-party block still themes, and they are listed under shadcn aliases. Use the kit names in your own markup so a retune never needs new class names.

Brand

bg-primary is the brand and the second brand hue is the support tone, so it reads as bg-tone inside data-tone="support". Each hue also has its tint, fill, and edge, which come from the tone and not from a token of their own.

LookPrimarySupport
DryInk (neutral 900)Neutral 700
WashiWave blueBlossom
ICSVioletOrange
RamsonPine greenRose
bg-primary
data-tone="support" bg-tone
data-tone="support" bg-tone-fill
data-tone="primary" border-tone

A filled Chip sits on the primary tone's fill with text-fg, and data-tone="support" flips it to the second hue. An outline chip uses the tone's edge with text-tone.

filled
outline

Brand as small text is text-tone-ink inside data-tone="primary", which is the brand mixed 48% toward the ink of the room. The mix is there so a mid-lightness brand such as Washi wave or ICS violet still reads as small text on paper and on bg-selected, which text-primary alone may not. Keep text-primary for icons and large type, and use bg-selected text-tone-ink in a primary tone for a brand-tinted chip.

<div className="bg-primary text-primary-foreground" />
<div data-tone="support" className="bg-tone text-tone-fg" />
<span data-tone="primary" className="bg-tone-fill text-fg">filled</span>
<span data-tone="primary" className="border-stroke border-tone/(--alpha-32) text-tone">outline</span>
<span data-tone="primary" className="bg-selected text-tone-ink">brand-tinted</span>

Do not use bg-secondary or bg-accent for a brand color, because those are shadcn aliases for bg-fill. Do not reach for a primitive such as bg-neutral-500, because primitives are not exposed as utilities. If a color is not a role, add the role.

Chrome

Chrome is one ladder of four steps: page, room, well, and card. Fill is a wash on top of the ladder and is not a step.

bg-background

bg-surface

bg-surface-sunken

Well. Tabs track, avatar fallback.

bg-surface-raised

Ink on raised paper.

Muted helper.

Subtle meta.

bg-fill

8% of ink in light, 10% in dark. Not a surface.

bg-selected

16% of primary in light, 22% in dark.

UtilityJob
bg-backgroundPage canvas
bg-surfaceRoom paper
bg-surface-sunkenThe tray behind the sidebar, and wells such as a toggle group track or a code block
bg-surface-raisedThe app's page, cards on it, and anything that floats: popover, menu, dialog
bg-fill and bg-fill-hoverInteractive wash: 8% and 16% of the contrast ink in light, 12% and 24% in dark
bg-selectedSelected row or nav item: 16% of the brand primary in light, 24% in dark
bg-track and border-trackUnfilled control edge, such as a switch rail or a checkbox and radio border: 32% of the contrast ink
text-fg, text-fg-muted, text-fg-subtleInk
border-borderThe outline of something static on the page, such as a card, an accordion's frame, or the sidebar edge: 4% of the contrast ink
border-border-strongOverlays, fields and controls, table and list rows, and separators: 8% of the contrast ink in light, 12% in dark

The page, cards, and anything that floats share the raised surface, so a card is a section of the page and a menu separates by its strong border and shadow. In light that is white, with fields a step down on bg-surface and the tray and wells a step below that. Dark mirrors it on half steps of the ramp: the page is neutral 925, and fields, the tray, and wells sit a half step lighter on 900, so the sidebar reads lighter than the page and a field reads as filled. Lines are washes of the contrast ink and not solid steps, so a line reads the same on the page, the tray, or a well. The contrast ink is the tonal opposite of the surfaces, so a wash darkens on light paper and lightens on dark paper. A look can repaint it, and Ramson sets it to pine so its washes and focus ring take a green tint.

Overlay scrims use bg-scrim/(--alpha-32), the shadow ink, so they are near-black in light and black in dark. See Elevation for how surfaces and shadows stack, and Chrome and temperature for the warm, cool, and gray paper options.

shadcn aliases

These are the same tokens under shadcn names. Kit markup uses the left column.

KitAlias, for third-party blocks
bg-surface-sunkenbg-muted
bg-surface-raisedbg-card, bg-popover
bg-fillbg-secondary, bg-accent
text-fgtext-foreground
text-fg-mutedtext-muted-foreground

bg-primary, bg-destructive, bg-background, and border-border are the same name in both vocabularies.

Alpha

--alpha-* is the one scale of wash weights, and it lives in the primitives. It is the one primitive a component may read, because a step is a weight and not a color. Every higher tier picks a step and never types a percentage. Use a step in color-mix as the percent of ink into transparent or into a surface, as a Tailwind alpha such as bg-primary/(--alpha-8), or as element opacity such as opacity-(--alpha-64). Do not write a bare number like /10 or opacity-50.

--alpha-4
--alpha-8
--alpha-12
--alpha-16
--alpha-24
--alpha-32
--alpha-48
--alpha-64
--alpha-82
--alpha-100
TokenWeightWhere the kit uses it
--alpha-44%The default border, in both modes.
--alpha-88%The default wash: --sys-fill in light, the strong border in light, the Progress track, a destructive menu item on focus, and status ink into surface-sunken.
--alpha-1212%--sys-fill in dark, the strong border in dark, and the tighter shadow layers in light.
--alpha-1616%A stronger wash: --sys-fill-hover and --sys-selected in light, the tone fill, an unchecked disabled Switch, Checkbox, and Radio, and the shadow-lg ambient layer.
--alpha-2424%--sys-fill-hover, --sys-selected, and the tone fill in dark, and the lightest shadow layer in dark.
--alpha-3232%--sys-track, overlay scrims, a dark shadow layer, the tone fill on hover, a checked disabled Switch, Checkbox, and Radio, and a category chip fill.
--alpha-4848%The brand and chip ink mixes, the deepest dark shadow layer, a category chip hairline, and a lifted editor card.
--alpha-6464%Disabled controls, the icon in a Chip, and the external-link arrow in the Markdown editor.
--alpha-8282%Hover on a solid Button or Badge, where the fill eases back a step.
--alpha-100100%The ink itself.

Two choices sit behind the steps:

  • Dark mode restates fill, hover, selected, the tone fill, and the strong border one step higher because light ink reads weaker on dark paper, and dark shadows triple their weight on a black ink so they show at all.
  • --sys-track is a light edge by choice, under the 3:1 WCAG 1.4.11 asks for on a control edge, so a switch, checkbox, or radio also carries its state in its shape and label. The categorical chip mixes are tuned for label contrast and described on Chip.

When a component needs a one-off wash, point a Tailwind alpha at a step and do not add a token, as in bg-contrast/(--alpha-16).

Overlay scrims are bg-scrim/(--alpha-32). bg-scrim is --shadow-color, a fixed dark ink that never inverts and never takes a brand, so a look that repaints the contrast ink leaves overlays alone. Switch, Checkbox, and Radio skip element opacity when disabled: the container drops to 16% when off and 32% when on, and the checked mark sits at 64%, so a disabled selection still reads as selected.

Status wash

Board lanes mix status ink at --alpha-8 into --sys-surface-sunken. That is the same step as fill, and the hue comes from the status and not from the contrast ink.

Todo

Lane wash

In progress

Lane wash

Blocked

Lane wash

Done

Lane wash

<div
  style={{
    background:
      "color-mix(in oklch, var(--sys-cat-green) var(--alpha-8), var(--sys-surface-sunken))",
  }}
/>

Status

There are four status colors: danger, success, warning, and info. Each is a tone, so the solid, its foreground, and its tint are bg-tone, text-tone-fg, and bg-tone-tint inside a data-tone scope, and bg-destructive stays as the shadcn fallback. Success, warning, and info use the green, yellow, and blue categorical hues, and danger is the red one. A tone tint is its solid mixed into the sunken well, at 8% in light and 16% in dark, and warning takes 16% in both because yellow washes out at 8%, so a look that changes a status solid changes its tint with it.

data-tone="danger" bg-tone
data-tone="success" bg-tone
data-tone="warning" bg-tone
data-tone="info" bg-tone
bg-tone-tint
bg-tone-tint
bg-tone-tint
bg-tone-tint

A look may retune the warm category hues, red, orange, and yellow, and danger, warning, and the chips that read them follow, as Washi does with a brick red. Green, blue, and the cool hues stay fixed, so success and info read the same in every look. Retune shows how.

Tone

A tone is a hue a component can wear, and one set of utilities draws every hue. A component reads bg-tone and text-tone-fg for a solid, bg-tone-tint for a quiet fill, text-tone-ink for small text, bg-tone-fill and bg-tone-fill-hover for a wash, and border-tone-edge for a line. A data-tone scope remaps them, so a Badge draws the same way in neutral, primary, support, danger, success, warning, and info, and Button, Badge, Alert, Progress, Switch, Checkbox, Radio group, and Segmented bar read it today, and so do the destructive items in Menu and Context menu.

ToneBaseForeground
neutralThe contrast inkThe page
primary--brand-primary--brand-primary-fg
support--brand-secondary--brand-secondary-fg
danger--destructive, which is --cat-red--destructive-foreground
success--cat-green--neutral-50
warning--cat-yellow--neutral-900
info--cat-blue, or a brand hue under data-info--neutral-50

Danger keeps the shadcn names because a shadcn block reads them, and a look overrides them. Success, warning, and info have no token of their own: each is set in its data-tone block and nowhere else.

The base is the hue itself, and the solid, bg-tone, is the base for every tone except success and info, where it is the base mixed toward neutral 900 so a white label works, 32% for success and 24% for info. Everything quiet derives from the base, so a deep solid never muddies it: the tint is the base at --alpha-8 into the sunken well, or --alpha-16 in dark and for warning, the ink is the base at --alpha-48 into the page ink, the fill is the base at --alpha-16, or --alpha-24 in dark, its hover is --alpha-32, and the edge is --alpha-48. neutral keeps the kit's own fill and ink, and its edge is the contrast ink at a weight an outline Button draws at 32%, which lands on the strong border. Outside any scope the tone is neutral.

A component sets its own data-tone, so a tone never leaks into a nested component: a Button inside a danger Alert is neutral unless it asks. A component's tone="inherit" leaves the attribute off, to take the scope around it. To add your own tone, set --tone-base, --tone-solid, and --tone-fg on a [data-tone="name"] block, unlayered, with --tone-solid: var(--tone-base) unless the label needs a deeper solid, and the tint, ink, fill, and edge follow.

Use a tone

Pick the tone by what the thing means and not by how it looks. Primary is the main action and the brand, and support is the second hue, for a thing that is a different kind of thing, such as a workspace name. Danger is for a destructive action or an error, and success, warning, and info are feedback about an outcome. Neutral is everything else.

Put data-tone on any element, and read the tone utilities inside it:

<div data-tone="success" className="rounded-md bg-tone-tint p-3">
  <p className="text-tone-ink">Payment received</p>
  <span className="rounded-control bg-tone px-2 py-1 text-tone-fg">Paid</span>
</div>

Payment received

Paid

Support is for the second hue, where a second brand-colored thing sits beside the primary and the two should read as different kinds of thing. A default chip and a support chip side by side is the usual case, such as a project next to the workspace it belongs to:

<Chip>Design system</Chip>
<Chip data-tone="support">Northwind</Chip>
Design system
Northwind

Keep the support solid for small marks, like an avatar or a count, and use the tint and ink for anything wider than a word or two.

Use bg-tone with text-tone-fg for a solid, bg-tone-tint with text-tone-ink for a quiet block, and text-tone-ink and not text-tone for small text, since the ink is tuned to read. A component that takes a tone prop sets the scope for you, so reach for data-tone only on your own markup.

ComponentTones it takes
Buttonneutral, primary, support, danger, and inherit
Badge, Progress, Switch, Checkbox, Radio groupEvery tone, and inherit
AlertIts variant sets info, success, warning, or danger
ChipPrimary, and data-tone="support" for the second hue

A Button takes no success, warning, or info, because those describe an outcome and not an action.

Accessibility

  • The focus ring uses the contrast ink and not the brand color, so it follows the paper and reads on every brand.
  • text-tone-ink is tuned in the source for contrast on paper for small brand text. Use it for that job and do not substitute text-primary or a lower alpha.
  • Status colors come in solid and foreground pairs, so use bg-tone with text-tone-fg and not an arbitrary ink.
  • Color tokens do not adapt themselves to forced colors. Components that need it add their own rules, for example Switch and Pagination, so check a new surface in forced colors before relying on a fill alone to carry meaning.

Next

Typography covers the ink roles in use, and Radius covers the other values a look changes. Retune covers how a look sets these colors.

On this page