Semantic Color Groups

Espalier's 23 semantic color tokens cluster into six higher-level groups that reflect how a UI is actually composed. Working with groups — rather than individual tokens — is the recommended approach for theme configuration and theme editors.

The five roles

The groups below are addressable in a theme as roles. A role is the unit a designer thinks in; the twenty-three semantic tokens underneath are the engine room.

Role What it paints Slots
canvas Page ground and raised surfaces color
ink Body copy, named type roles, and headings color, heading
accent Decoration — hover washes, carets, selection grounds color, text, hover
action Filled action surfaces and their labels color, ink
structure Borders and shadows color

A role binds a color source — a geometric family, a status family, or a brand anchor (see Theming). The test of the layer is linguistic: a sentence a designer would say should be a line of theme.

const theme = {
  anchors: {
    plum: '#78486A',
    espresso: '#4A3327',
    cream: '#FFF1D9',
    rose: { color: '#B76E79', text: '#9B4D59' },
  },
  roles: {
    canvas: 'anchor:cream',                                    // cream is the page
    ink: { color: 'anchor:espresso', heading: 'anchor:plum' }, // espresso is body copy, plum heads
    accent: { color: 'anchor:rose', text: 'anchor:rose.text' },// rose is decorative only
    action: 'anchor:plum',                                     // white on plum for reversed sections
  },
};

Four sentences, four lines, twenty-three correct tokens in both schemes.

Accent is not action

Decoration and action are separate roles because real brands separate them. In the example above rose decorates — rules, icons, hover washes — while plum acts. A system that treats "the accent color" as one thing cannot express that brand at all.

Decorative-only is structural

The accent role has no body-text slot. That is the model enforcing "rose is decorative only" rather than a comment asking you to remember it: when the accent family must carry copy, it comes through accent.text, the deepened variant chosen for legibility. Naming a slot the role does not have is a validation error.

Surfaces bring their own ink

action paints a surface, so it also answers "what writes on it". If a theme does not name action.ink, the label derives from the canvas color and APCA enforcement guarantees it is legible against the action surface — which is how white on plum for reversed sections becomes one line instead of a hand-picked hex.

Two details worth knowing. The action surface is placed by lightness rather than by ramp name, because the ramp inverts between schemes: the stop that hosts a reversed band is muted in a light theme and raised3 in a dark one. Among the stops whose finished, gamut-mapped surface can carry the action label's Lc target, the role picks the one nearest the intended reversed-band lightness. The paired ink then starts at the ramp's extreme in the direction contrast enforcement pushes, so enforcement has the strongest ink and the least distance to travel.

This feasibility check uses the same variant, chroma, anchor, semantic, gamut, and APCA functions as the property pipeline. APCA enforcement still owns the final label value. If a custom ramp makes every stop incapable of meeting the target, the deterministic nearest-stop fallback keeps producing a theme and the theme fit report names the shortfall and shows the Lc it reached.

Roles, tokens, and what a theme editor should expose

Safe product-level choices Keep internal to Espalier
The five roles and their slots The twenty-three semantic tokens
Brand anchors Exact lightness stop per token
Optionally: the lightness ramp APCA target calculation and surface/ink pairing

A theme editor should expose roles. Token-level semanticMappings remain available as the engine-room override for cases roles do not reach — but a theme editor that starts by exposing all 23 tokens is handing users the engine room instead of the dashboard.

The tokens each role compiles to

Useful when reading a resolved theme or overriding a single token.

Role and slot Tokens
canvas.color background, layer1, layer2, layer3, layer4
ink.color text, typeOverline, typeLabel, typeLead, typeCaption, inputSelection
ink.heading headings, headingsHover
accent.color linkHoverBg, inputSelectionBg, inputCaret
accent.text link
accent.hover linkHover (falls back hovertextcolor, so an undeclared hover keeps following accent.text)
action.color actionBackground
action.ink actionText
structure.color border, shadow

dangerText belongs to no role: the status families' meanings are reserved — no role may reassign what danger, success, warning, or info signify. Their colors are a brand decision since 3.1: retune a family with the theme's intents field ("intents": { "danger": "#700007" }), and the derived tokens such as dangerText follow (see Theming, ADR-004).

Tokens that must move together

Some tokens are contrast-enforced pairs and must not be remapped independently:

  • actionBackground and actionText — remapping one without the other breaks the APCA-guaranteed contrast between button background and label.
  • inputSelection and inputSelectionBg — these are an emphasis-ink plus emphasis-background pair.
  • link and linkHover — each is contrast-enforced (Lc 75) against the surface it actually renders on: link against the page background, and linkHover against linkHoverBg, the wash hovered links paint on (since 3.3; pairing it with the page background left hovers under tier on the rendered wash). Since 3.1 they need not share a family: accent.hover lets the hover change family independently (hovertextcolor fallback). linkHoverBg is the accent.color wash behind the hovered link; keep it working with whichever family the hover uses.

Light and dark scheme guidance

  • Light schemes can tolerate a wider spread between surface and text lightness.
  • Dark schemes should keep surface families more neutral and compressed.
  • In dark mode, prefer brighter accents rather than broader accent coverage on large surfaces — a lighter accent occupying less area reads louder without overwhelming the composition.
  • If a dark theme feels flat, raise accent lightness before expanding accent coverage.

The status role is reserved

dangerText — and by extension the danger color family — carries universal semantic meaning (errors, destructive actions). It must not be repurposed for decorative emphasis or remapped to a brand family. The same principle applies to success and warning family usage in components.

Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O