Using Espalier Tokens, Intents, And Contexts Coherently
Espalier gives application authors three different styling levers:
| Lever | Use it when | Typical examples |
|---|---|---|
Theme configuration on <esp-root> |
The whole product should change together. | seedColor, semanticMappings, typeRatio, spaceRatio, font families |
| Component-level CSS properties | A specific component instance needs a local override. | --esp-tooltip-max-width, --esp-progress-height, --esp-header-background |
intent on a component |
The element carries meaning — success, warning, danger, info. | intent="success", intent="danger" |
context on a container |
A zone of the page uses a theme-defined rebinding — the inverted footer, the quiet band. | context="inverted" |
When these are used in the right order, the system stays coherent. When they are mixed casually, the result is usually brittle CSS and inconsistent color semantics.
Start With Semantic Tokens
Prefer semantic tokens such as --esp-color-text, --esp-color-border, --esp-color-action-background, and --esp-color-link over raw variant colors such as --esp-color-complementary or --esp-color-triadic-left.
Semantic tokens answer questions like "what is the text color here?" or "what is the action background here?" Raw variant tokens answer "which hue family are we using?" In application code, the semantic question is usually the one you want.
Use raw variant colors when you are:
- configuring the theme itself
- building a new semantic mapping
- creating a deliberate accent effect that is not part of a component's normal semantic contract
Keep Sizing Font-Relative
Espalier sizing is designed to scale with typography. Prefer these token families:
--esp-type-*for text scale--esp-size-*for spacing--esp-size-font,--esp-size-padding, and--esp-size-padding-pagefor common aliasesremandemfor custom measurements that should grow with the system
Avoid hard-coded pixel values for layout, spacing, and typography unless you are dealing with a genuinely fixed physical detail such as a 1px hairline, a bitmap asset boundary, or a very small icon stroke.
Understand What intent And context Change
Espalier 3 splits the retired variant attribute into two attributes that
carry its two unrelated jobs:
intent states what an element means — neutral, success,
warning, danger, or info. On token-emitting controls such as
buttons, a non-neutral intent pins the element's filled-action pair
(--esp-color-action-background / --esp-color-action-text) to the
intent's own fixed status family — red-orange, green, yellow-green, and
blue by default, each retunable per theme via intents — with contrast
enforced against the actual action surface. Class-styled chrome (esp-badge, esp-info,
esp-status-indicator) renders its intent treatment from CSS classes
instead and emits no inline tokens. An intent never repaints the
element's surroundings: a danger button on a cream card stays on the
cream card.
<esp-button intent="danger" label="Delete forever"></esp-button>
<esp-badge intent="success">Published</esp-badge>
context selects a theme-defined zone — the inverted footer, the
quiet band — and re-emits the complete semantic table for that subtree,
re-derived and contrast-enforced against the zone's own surfaces. Zones
are declared in the theme; see the theming guide.
<esp-box context="inverted">
<h3>Reversed section</h3>
<p>Zone-correct tokens for every descendant, slotted HTML included.</p>
<esp-button intent="danger" label="Delete"></esp-button>
</esp-box>
The two compose: an intent inside a context keeps its status hue and its
contrast against the zone's surfaces. Use intent for meaning, context
for zones, and neither for an isolated style tweak.
Change Component Props Before Local Root Tokens
If a component already exposes a component-level CSS property, prefer that property over overriding a root semantic token inside the component subtree.
Good:
.upload-progress {
--esp-progress-fill-color: var(--esp-color-success);
}
Avoid:
.upload-progress {
--esp-color-action-background: var(--esp-color-success);
}
The first change is local and explicit. The second changes a semantic root token for every descendant, which can accidentally affect text, links, nested components, or future styling additions.
Change The Theme When The Language Should Shift Everywhere
If multiple components need the same new visual language, change the theme rather than stacking many CSS overrides.
Good theme-level changes include:
- changing the seed color
- adjusting
semanticMappings - tightening or loosening chroma ranges
- changing
typeRatio,spaceRatio, or font families - setting shared background-image fields for pages, boxes, or vellum overlays
const lightTheme = {
seedColor: "oklch(0.68 0.14 210)",
typeRatio: 1.2,
spaceRatio: 1.5,
semanticMappings: {
link: { source: "triadic-left", lightness: "accent" },
},
};
This keeps the whole system internally consistent. It is usually better than overriding a scattered list of emitted --esp-color-* outputs in CSS.
When You Touch semanticMappings, Think In Groups
semanticMappings is the engine's low-level contract, but product theming should usually move coherent groups together rather than editing isolated tokens one by one.
Start with the grouped model from Semantic Color Groups:
- foundation surfaces
- structure and chrome
- content ink and hierarchy
- action surfaces
- inline emphasis and feedback
- utility and status
That model helps you decide which mapping choices are safe to expose in an application editor and which ones should remain internal to Espalier's contrast and state logic.
Good Patterns And Brittle Patterns
| Prefer | Avoid |
|---|---|
var(--esp-size-small) or var(--esp-type-medium) |
Literal 14px, 22px, or large spacing values copied from another app |
intent="success" on a status-bearing control |
Manually setting several --esp-color-* properties on one component to fake a status treatment |
Component-prefixed props such as --esp-tooltip-max-width |
Inventing new generic globals such as --esp-color-text-muted for one component |
Theme config changes on <esp-root> for shared brand shifts |
Re-theming the app by overriding many emitted root tokens one by one |
Semantic colors such as --esp-color-text and --esp-color-border |
Raw variant colors for ordinary body text, borders, and form surfaces |
Guidance For AI Authors
If you are generating Espalier code with AI, these defaults produce the least brittle output:
- Prefer existing Espalier tokens over literal colors and measurements whenever a matching token family exists.
- Prefer semantic colors over raw variant colors in application CSS.
- Prefer a theme-defined
contextwhen a whole subtree should shift palettes together, andintentfor an element's status. - Prefer component-level custom properties over locally overriding
--esp-color-*outputs. - Prefer theme configuration on
<esp-root>when the desired change should apply across the product. - If a component lacks the styling hook you need, call that out as a library gap instead of inventing an undocumented global token.
- Do not target
--_esp-*tokens. Those are private implementation details, not supported application-level hooks.
That last rule matters. Undocumented globals feel convenient in the short term, but they make future theming and component evolution much harder.