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:
- Design-token contract test — verifies that every
var(--esp-*)reference in source has a matching declaration,@csspropannotation, runtime setter, or root-theme emitter. - Styling-guide coverage test — ensures
docs/guides/styling.mddocuments every public token and lists no unknown ones. - Token manifest sync test — verifies that
espalier.token-manifest.jsonmatches 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
- Styling Reference — complete table of every CSS custom property
- Tokens, Intents & Contexts — when to use theme fields, component props,
intent, orcontext - Semantic Color Groups — role-based grouping model for the 23 semantic colors
- Perceptual Color Engine — OKLCH, APCA, and how the palette is computed
- URL Help System — help-source inheritance, stable topic ids, provider setup, and the normalization contract