AI Development Guide for Espalier Consumers

AI coding tools produce better results when they understand the design system they are working with. This guide explains the Espalier token contract, lists the mistakes AI tools make most often, and provides copy-pasteable instructions you can drop into your project's CLAUDE.md, AGENTS.md, or .github/copilot-instructions.md.

It also shows how to set up a lightweight automated check that catches invalid token references before they reach production.


The Espalier Token Contract

Espalier exposes CSS custom properties at three levels. Understanding which level to target is the single most important thing an AI tool needs to get right.

1. Root-theme tokens

Computed by <esp-root> and inherited by every descendant. These are the design system's foundational values.

Prefix Examples
--esp-color-* --esp-color-text, --esp-color-border, --esp-color-action-background
--esp-type-* --esp-type-normal, --esp-type-large
--esp-size-* --esp-size-padding, --esp-size-small-to-normal
--esp-font-* --esp-font-body, --esp-font-weight-headings
--esp-l-* --esp-l-surface, --esp-l-text (lightness ramp)

These tokens should be consumed, never overridden in application CSS. To change them system-wide, update the theme object passed to <esp-root>.

2. Shared hooks

Cross-component tokens used by field-like controls and overlays.

Token Purpose
--esp-field-background Background for inputs, selects, textareas
--esp-field-border-color Border for field controls
--esp-field-text-color Text inside field controls
--esp-vellum-background Overlay backdrop color

These can be overridden on a per-section basis when you need a group of fields to share a non-default appearance.

3. Component-level tokens

Scoped to individual components for targeted overrides.

/* Override tooltip width for a specific context */
.my-panel {
  --esp-tooltip-max-width: 60ch;
}

Every component's tokens are listed in the Styling Reference and in the published espalier.token-manifest.json.

Private tokens

Tokens prefixed with --_esp- are private implementation details. They are not part of the public contract, may change without notice, and must never be referenced or overridden.

Intent and context

When an element carries a status, set the intent attribute instead of overriding individual color tokens; when a subtree needs a different palette, select a theme-defined context. This keeps semantic roles intact:

<!-- Correct: intent carries the element's meaning -->
<esp-button label="Save" intent="success"></esp-button>

<!-- Wrong: manually overriding breaks semantic coherence -->
<esp-button label="Save" style="--esp-button-background: green;"></esp-button>

For more detail on when to use each lever, see Tokens, Intents & Contexts.


Common AI Anti-Patterns

Hardcoded colors instead of tokens

/* Wrong */
.card { color: #333; background: white; }

/* Correct */
.card { color: var(--esp-color-text); background: var(--esp-color-layer-1); }

Ad-hoc oklch() mixed with the token system

/* Wrong — breaks when the theme changes */
.badge { background: oklch(0.7 0.15 150); }

/* Correct — derive from the design system */
.badge { background: var(--esp-color-action-background); }

Overriding semantic tokens that should stay internal

/* Wrong — bypasses APCA contrast enforcement */
:root { --esp-color-text: navy; }

/* Correct — change the theme configuration instead */

Theme-level tokens are computed from a seed color through perceptual color math and contrast enforcement. Overriding them directly breaks the pipeline.

Fabricating token names

/* Wrong — this token does not exist */
.sidebar { padding: var(--esp-size-sidebar-padding); }

/* Correct — use a real token from the contract */
.sidebar { padding: var(--esp-size-padding); }

AI tools sometimes invent plausible-sounding token names. Every reference should be validated against the published manifest or Styling Reference.

Ignoring intent

/* Wrong — hardcodes a status color */
.alert { border-color: var(--esp-color-danger); }

/* Correct — use intent for status styling */
<esp-info intent="danger">Something went wrong.</esp-info>

Prompt Fragments

Copy these into your project's CLAUDE.md, AGENTS.md, or .github/copilot-instructions.md. Adjust paths and project names as needed.

Core instructions

## Espalier Design System

This project uses the Espalier design system (`@taprootio/espalier`).

### Token rules

- Use `--esp-*` CSS custom properties for all colors, spacing, typography,
  and layout values. Never hardcode hex, rgb, oklch, or pixel values when
  a token exists.
- Never override root-theme tokens (`--esp-color-*`, `--esp-type-*`,
  `--esp-size-*`, `--esp-font-*`, `--esp-l-*`) in application CSS.
  To change them, update the theme object on `<esp-root>`.
- Never reference or override private tokens (`--_esp-*`).
- Use component-level tokens (`--esp-{component}-*`) for targeted
  overrides. Check the Espalier Styling Reference to confirm the token
  exists before using it.
- Use the `intent` attribute for status styling and theme-defined
  `context` zones for palette shifts. Do not manually override color
  tokens to simulate either.

### Verification

- Run the consumer token check (see below) before considering styling
  work complete.
- Reference `node_modules/@taprootio/espalier/espalier.token-manifest.json`
  as the source of truth for valid token names.

Extended instructions (with layering model)

### Espalier layering model

Espalier uses a three-tier token hierarchy:

1. **Theme fields** — configured on `<esp-root>` (seed color, font
   families, scale ratios, semantic mappings). Change these for
   system-wide shifts.
2. **Semantic tokens** — emitted as CSS custom properties by
   `<esp-root>` (`--esp-color-text`, `--esp-size-padding`, etc.).
   Consume these in application CSS but never override them.
3. **Component hooks** — scoped tokens for individual component
   overrides (`--esp-tooltip-max-width`, `--esp-grid-header-background`).
   Override these when a specific component instance needs a local change.

When deciding which level to target:
- "The whole product should look different" → theme fields
- "This one button should be wider" → component hook
- "This section should use a different color family" → theme-defined `context`

URL help-system instructions

Use this fragment when the project publishes application-owned help documents:

### Espalier URL help system

- Place exactly one `<esp-help-provider>` inside the application's
  `<esp-root>` and keep one shared `<esp-flyout>` in the page's `flyout`
  slot.
- Declare `help-src` once on the nearest form, page, feature, or section
  ancestor instead of repeating the document URL on every trigger.
- Give every helped `esp-form-item` a stable `field-name`. Its help anchor
  is derived from that value, so the help document must contain a heading
  with the identical `id`.
- Use `help-url` only for exceptions. A fragment in `help-url` overrides
  the form item's derived anchor.
- Use `<esp-help-button anchor="…" label="Help for …">` for page,
  feature, section, and dialog topics. The label must identify the topic;
  do not use a generic unlabeled icon.
- Help URLs must return HTML. Markdown or CMS content is acceptable as an
  authoring source only after its publishing pipeline renders HTML.
- Treat help documents as trusted application content. Espalier strips
  presentation attributes for visual consistency but does not sanitize
  untrusted HTML.
- Preserve heading ids during documentation refactors. A missing id falls
  back to the full document and produces an anchor-drift warning.

Example task prompt:

Wire this account form to `/help/account.html` with Espalier's URL help
system. Put `help-src` on the shared ancestor, keep each form item's
`field-name` aligned with a stable heading id in the help document, use
persistent `hint` text only for short always-visible guidance, and add a
standalone `esp-help-button` for the overall account topic. Do not repeat
the document URL on each field unless it is a genuine override.

Consumer-Side Contract Testing

The published espalier.token-manifest.json lets you verify that your CSS only references valid Espalier tokens. Here is a minimal test you can drop into any project that uses Vitest (adapt for Jest or another runner as needed).

Install

The manifest ships inside the @taprootio/espalier package. No extra install is required.

Test file

// __tests__/espalier-token-contract.test.js
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, it, expect } from "vitest";
import manifest from "@taprootio/espalier/espalier.token-manifest.json" with { type: "json" };

const VALID_TOKENS = new Set(manifest.tokens.map((t) => t.name));
const TOKEN_RE = /var\(\s*(--esp-[a-z0-9-]+)/g;

/** Recursively collect files matching an extension. */
function walk(dir, ext, files = []) {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, entry.name);
    if (entry.isDirectory() && !entry.name.startsWith("node_modules")) {
      walk(full, ext, files);
    } else if (entry.isFile() && full.endsWith(ext)) {
      files.push(full);
    }
  }
  return files;
}

describe("espalier token contract", () => {
  it("only references valid Espalier tokens", () => {
    // Adjust these paths to match your project structure.
    const files = [
      ...walk("src", ".ts"),
      ...walk("src", ".css"),
    ];

    const invalid = [];

    for (const file of files) {
      const text = readFileSync(file, "utf8");
      TOKEN_RE.lastIndex = 0;
      let match;
      while ((match = TOKEN_RE.exec(text))) {
        if (!VALID_TOKENS.has(match[1])) {
          invalid.push({ token: match[1], file });
        }
      }
    }

    if (invalid.length > 0) {
      const summary = invalid
        .map((i) => `  ${i.token} in ${i.file}`)
        .join("\n");
      expect.fail(
        `Found references to unknown Espalier tokens:\n${summary}`,
      );
    }
  });
});

This test scans your source for var(--esp-*) references and compares each one against the manifest. Unknown tokens fail the test with a clear message showing the token name and file.

CI integration

Add the test to your existing test suite or run it as a dedicated check:

npx vitest run __tests__/espalier-token-contract.test.js

How Espalier Protects Its Own Contract

Espalier runs three internal checks in CI:

  1. Design-token contract test — verifies that every var(--esp-*) reference in source has a matching declaration, @cssprop annotation, runtime setter, or root-theme emitter.
  2. Styling-guide coverage test — ensures docs/guides/styling.md documents every public token and lists no unknown ones.
  3. Token manifest sync test — verifies that espalier.token-manifest.json matches the source-derived token set. A stale manifest fails CI.

These run via npm run verify-design-tokens and block merges when tokens drift out of sync. The consumer test described above is the downstream equivalent — it verifies your application against the same contract that Espalier enforces internally.


Further Reading

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