Colors

The palettes, the role vocabulary every hue shares, and where each is meant to be used.

The palettes#

Ten hues plus brand, each with eleven steps from 50 (near white) to 950 (near black). These are the primitives, the only real color values in the system; everything else is a pointer to one of these.

gray
red
orange
yellow
green
teal
blue
cyan
purple
pink

brand is special: by default it aliases the gold palette, but it's the one palette that can be replaced at runtime: pass tokens={{ brandColor }} to the provider and all ten brand steps are recomputed from your color (see Theming).

Roles: how components actually pick colors#

Components rarely touch a raw step like blue-600. Every palette exposes the same seven roles, and those are what variant styles are written against:

RoleJobExample use
solidThe strong fillSolid button background
contrastText on top of solidSolid button label
labelTinted text on neutral backgroundsLink text, subtle-button label
subtleFaint tinted backgroundBadge background, hover wash
mutedA step stronger than subtleSubtle-button hover
strokeTinted borderOutline button border
ringFocus ringAny focused control

Two things make this vocabulary powerful:

  • Light and dark values differ per role. In light mode blue-subtle points at blue-100; in dark mode it re-points at a deep step. Write against roles and dark mode is automatic.
  • Every hue answers the same questions. Because all palettes share the vocabulary, one set of component styles can be recolored to any hue. That's what makes colorScheme possible.

The accent channel#

accent-* is a virtual palette. It has the same seven roles but no colors of its own: it forwards to whichever real palette the nearest colorScheme chose (brand by default). Components are written once against accent-* and recolor through it. The full mechanism is explained on the Theming page.

Neutral semantic tokens#

For everyday UI structure (page backgrounds, text, borders), use the neutral roles instead of any palette:

FamilyTokensJob
Surfacebase · subtle · muted · raised · invertedSee the ladder below
Labelbase · muted · subtle · invertedSee the label ladder below
Strokebase · subtle · muted · invertedSee the stroke ladder below

The surface ladder#

Three neutrals, one rule each. You should never have to guess which to reach for.

TokenWhen
baseThe page, and any control at rest: an input, a checkbox, an active tab
subtleSomething sitting on the page but not floating: a card, a table header, an inactive item
mutedNot interactive: disabled controls (with reduced opacity), and inert parts like a progress track or slider rail

Hover is one step up the ladder from wherever the element rests. A control resting on base hovers to subtle; something already at subtle hovers to muted. There is no separate hover token to memorise, and no component gets to pick its own answer.

Two things the ladder does not cover. raised is not a fourth neutral: it is identical to base in light mode and one step lighter in dark, where a shadow cannot read. Floating layers only: menus, toasts, popovers, modals, drawers, listboxes. Cards never use it. And inverted is for the few things that must read against any surface at all: tooltips, the slider value bubble.

The label ladder#

Three tiers of text, ordered by contrast against the page.

TokenWhen
baseBody copy, headings, anything you actually read
mutedSecondary text: descriptions, captions, help text
subtleThe faintest tier that is still content: placeholders, empty states, listbox group headers, calendar weekday rows

Every tier clears WCAG AA (4.5:1) on the page it sits on, in both modes. subtle is deliberately the floor rather than "as light as it can go": it paints placeholder text, and a placeholder nobody can read is a bug, not a style. If you want something genuinely decorative and unreadable, reach for opacity, not a lighter label token.

One asymmetry worth knowing: dark is spaced one ramp step wider than light. The neutral ramp has nothing between gray-400 and gray-500 on a near-black page, so holding muted where light has it would have pushed subtle under the AA line.

inverted is the fourth: text on an inverted surface, used by Tooltip and the slider value bubble.

The stroke ladder#

Strokes work differently, and the difference is deliberate. A default background is the absence of tint, so surfaces can only ever step up from base. A default border has to be visible, so stroke-base sits in the middle, with one step either side of it.

TokenWhen
subtleA dividing line inside something that is already bordered: a card header rule, a drawer footer
baseEvery default border, and every control outline: cards, tables, inputs, checkboxes, panels, Separator
mutedStronger than the default: the hover border on an outline input

So the direction reads subtle < base < muted, ordered by contrast against the page in both light and dark. Hovering an outline control still means "one step up", exactly as it does for surfaces. It just starts from base rather than ending there.

Status colours are palettes, not neutrals#

error, warning, success and info are not part of the three families above. Those families are ladders measuring emphasis; a status colour carries meaning, which is a different axis entirely. So status lives where every other colour lives: as a palette with the same seven roles:

<Text color="error" />       {/* error-label: the text role    */}
<Box bg="success" />         {/* success-solid: the fill role   */}
<Box borderColor="warning" />{/* warning-stroke: the line role  */}

These are the same tokens colorScheme="error" rebinds onto the accent channel, so Alert, Toast and form validation all resolve to one set of values rather than two that drift apart.

Naming a hue picks the role that fits the property#

Every palette works this way, not just the status ones. The bare hue resolves to whichever role the CSS property actually wants (solid for a fill, label for text, stroke for a line), so the common case is one word:

Bare formAlso available
bg"blue" → blue-solidblue-subtle · blue-muted · blue-50–blue-950
color"blue" → blue-labelblue-solid · blue-contrast · blue-50–blue-950
borderColor"blue" → blue-strokeblue-50–blue-950

Two things worth knowing. gray has no bare form: its solid is near-black in light and white in dark, which is never what bg="gray" would suggest, and the neutral ladder already covers those cases. And the numbered shades are the escape hatch, not the default: they are the only colour tokens that do not re-point in dark mode, so bg="blue-500" freezes one mode's decision into both.

Using colors in your own code#

Style props accept all three tiers:

<Box bg="subtle" />            {/* neutral semantic role */}
<Box bg="blue-subtle" />       {/* palette role */}
<Box bg="blue-100" />          {/* raw primitive, last resort */}
<Text color="muted" />         {/* neutral label role */}

Prefer the highest tier that says what you mean: semantic roles adapt to both themes on their own, palette roles adapt within their hue, and raw steps don't adapt at all. In custom CSS, the same values are available as variables: var(--astralis-color-blue-subtle), exactly like the swatch grid demo above does.