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 hover → text → color, 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:
actionBackgroundandactionText— remapping one without the other breaks the APCA-guaranteed contrast between button background and label.inputSelectionandinputSelectionBg— these are an emphasis-ink plus emphasis-background pair.linkandlinkHover— each is contrast-enforced (Lc 75) against the surface it actually renders on:linkagainst the pagebackground, andlinkHoveragainstlinkHoverBg, 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.hoverlets the hover change family independently (hover→text→colorfallback).linkHoverBgis theaccent.colorwash 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.