Using Espalier Tokens And Variants 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 |
variant on a component |
A whole component subtree should speak in a different color family while still using the same semantic roles. | variant="success", variant="warning" |
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 variant Changes
On components that extend EspalierElementBase, variant is more powerful than a single color swap:
- Espalier resolves the chosen variant color from the active theme.
- It points the component's local
--esp-color-primaryat that variant. - It recomputes semantic tokens for that component subtree so backgrounds, text, links, borders, and action colors stay related to the new hue family.
That means variant="warning" is a coherent palette shift, not just a different border color.
<esp-box variant="warning">
<h3>Review Before Continuing</h3>
<p>This section keeps warning-tinted semantics for its descendants.</p>
<esp-button label="Resolve issue"></esp-button>
</esp-box>
Use variant when the whole component should feel success-oriented, dangerous, complementary, or otherwise shifted as a unit. Do not use it when you only want one 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 |
variant="success" on a cohesive success surface |
Manually setting several --esp-color-* properties on one component to fake a success variant |
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
variantwhen a whole component or subtree should shift hue families together. - 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.