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-page for common aliases
  • rem and em for 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:

  1. Espalier resolves the chosen variant color from the active theme.
  2. It points the component's local --esp-color-primary at that variant.
  3. 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 variant when 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.

Where does this show?