Architectural Alchemy: The Espalier Theming Engine
A static palette is a snapshot. A theming engine is a living organism — one that breathes with user preference, adapts to ambient light, and reshapes itself from a single seed into an entire visual language. The Espalier theming system transforms a handful of design intentions into 70+ CSS custom properties, computed in real time using perceptual color science, modular scales, and APCA contrast enforcement.
The Seed: One Color to Rule Them All
Every Espalier theme begins with a seed color — a single OKLCH value from which the entire palette is derived. The engine rotates, scales, and maps this seed through geometric color theory to produce eleven color variants:
- Primary — the seed itself, unrotated.
- Analogous Left / Right — harmonious neighbors (default ±30°).
- Complementary — maximum contrast (default +180°).
- Split-Complementary Left / Right — a softer tension (180° ± 30°).
- Triadic Left / Right — energetic three-point harmony (±120°).
- Danger / Success / Warning — fixed semantic hues (27°, 150°, 90°) that remain stable regardless of the seed.
Every rotation angle is configurable. Push analogous to 45° for wider spread. Narrow triadic to 90° for a tighter triad. The geometry is yours.
The Lightness Ramp: Eleven Perceptual Stops
Where the seed controls hue, the lightness ramp controls depth. Eleven named stops map to perceptual roles:
| Stop | Light Default | Dark Default | Role |
|---|---|---|---|
surface |
0.99 | 0.15 | Page background |
raised1 |
0.97 | 0.20 | First elevation layer |
raised2 |
0.90 | 0.30 | Second elevation layer |
raised3 |
0.80 | 0.40 | Third elevation / action bg |
raised4 |
0.72 | 0.36 | Fourth elevation |
accent |
0.50 | 0.60 | Links, interactive highlights |
muted |
0.45 | 0.75 | Headings, subdued content |
text |
0.35 | 0.85 | Body text |
border |
0.30 | 0.70 | Borders, dividers |
ink |
0.25 | 0.95 | High-contrast ink |
shadow |
0.20 | 0.60 | Drop shadows |
Notice the inversion: light schemes descend from bright surfaces to dark text; dark schemes ascend from deep surfaces to bright text. Each value is an OKLCH Lightness (
Semantic Mappings: Color Meets Purpose
The engine maps nineteen semantic tokens — background, text, link, headings, actionBackground, border, shadow, and more — to a combination of color source (which variant supplies the hue) and lightness stop (which ramp position sets perceived brightness).
For example, the default mapping sends link to complementary at the accent lightness, creating a vibrant interactive color that naturally contrasts with primary-hued body text. Headings draw from primary at muted, keeping them visually anchored but clearly differentiated from body copy.
Every mapping is overridable. Route headings through triadic-left for editorial flair. Push actionBackground to analogous-right for warmer call-to-action surfaces. The semantic layer is the bridge between raw color math and design intent.
At the engine level, that bridge is still a flat table. For application authors and theme editors, the more useful framing is a grouped one: foundation surfaces, structure and chrome, content ink, action surfaces, inline emphasis and feedback, and utility or status colors. The Semantic Color Groups guide explains which tokens stay coupled, which source families are the safest defaults, and which mapping choices should remain internal to Espalier.
Chroma Clamping: Controlling Saturation
Each semantic token carries a chroma range — a { min, max } pair that constrains color intensity before gamut mapping. The defaults are permissive (0 to 0.4), but tightening these ranges is one of the most powerful theming tools available:
- Set
max: 0.05onbackgroundandlayer1for near-neutral surfaces that let content breathe. - Set
min: 0.15onlinkto guarantee links never appear washed-out. - Clamp
headingsto{ min: 0.08, max: 0.12 }for subtle, sophisticated titles.
Chroma clamping works in concert with APCA contrast enforcement — the engine adjusts lightness after clamping to ensure text tokens always meet their target
Variant Chroma: Independent Saturation per Color Source
By default, every color variant inherits the seed color's chroma — if your seed is oklch(0.7 0.125 216), all eleven variants start at chroma 0.125 before lightness and clamping are applied. The variantChroma field lets you override this inheritance for any of the ten non-primary color sources, giving each variant its own base saturation.
The ten overridable sources are:
| Source | Description |
|---|---|
analogous-left |
Harmonious neighbor (left rotation) |
analogous-right |
Harmonious neighbor (right rotation) |
complementary |
Maximum hue contrast |
split-complementary-left |
Softer complement (left) |
split-complementary-right |
Softer complement (right) |
triadic-left |
Three-point harmony (left) |
triadic-right |
Three-point harmony (right) |
danger |
Fixed red-orange semantic hue |
success |
Fixed green semantic hue |
warning |
Fixed yellow-green semantic hue |
Any source you omit from variantChroma inherits the seed chroma automatically. This means you only need to specify the overrides you care about:
{
seedColor: "oklch(0.7 0.125 216)",
variantChroma: {
// Push complementary and triadic colors to higher saturation
"complementary": 0.22,
"triadic-left": 0.20,
"triadic-right": 0.20,
// Keep danger vivid, mute success and warning
"danger": 0.25,
"success": 0.08,
"warning": 0.08,
// analogous-left, analogous-right, split-complementary-left,
// split-complementary-right all inherit 0.125 from the seed
}
}
Variant chroma operates upstream of per-token chroma clamping. The pipeline is:
- Variant chroma sets the base chroma for each color source (override or seed).
- Semantic mapping selects which source feeds each token.
- Chroma clamping (
chroma) constrains the token's output range. - Gamut mapping pulls out-of-sRGB colors back into displayable space.
- APCA contrast enforcement adjusts lightness for text legibility.
This two-level chroma control — variantChroma for the input and chroma for the output — gives you precise control over both the raw intensity of each color family and the refined saturation of each semantic role.
Typography & Scale
Theming extends beyond color. Each theme controls:
fontBody,fontHeadings,fontMonospace— CSSfont-familystrings for body text, headings, and code blocks.fontWeightBody— CSSfont-weightfor body and UI text (default"normal"). Emitted as--esp-font-weight-body.fontWeightHeadings— CSSfont-weightfor headings (default"bold"). Emitted as--esp-font-weight-headings.fontWeightMonospace— CSSfont-weightfor code and monospace text (default"normal"). Emitted as--esp-font-weight-monospace.typeRatio— the modular ratio for the type scale (default 1.25, Major Third). This generates seven fluid steps from--esp-type-tinyto--esp-type-hugeusing CSSclamp().spaceRatio— the modular ratio for the spacing scale (default 1.618, Golden Ratio). Seven spacing steps plus six cross-step fluid sizes.borderRadius— global corner rounding in rem.rootFontSize— the base font size in px.
Font weights accept any valid CSS font-weight value — keywords ("normal", "bold", "lighter") or numeric strings ("400", "700", "900"). Numeric values from JSON (e.g. 400) are automatically coerced to strings. This lets you pair a light-weight body font with heavy display headings, or vice versa:
{
fontBody: '"Inter", sans-serif',
fontWeightBody: "300", // Light body text
fontHeadings: '"Bebas Neue", sans-serif',
fontWeightHeadings: "900", // Black headings
fontMonospace: '"Fira Code", monospace',
fontWeightMonospace: "400", // Regular-weight code
}
Pair a geometric sans-serif body with a high-contrast display heading font. Tighten the type ratio to 1.2 for dense data UIs, or push it to 1.333 for dramatic editorial layouts. The scale engine interpolates fluidly between viewportMin and viewportMax.
Background Tiles: Texture as Identity
Espalier themes support optional background images on both the page surface and esp-box components:
pageBackgroundImage— a CSSbackground-imagevalue (URLs, gradients, or inline SVG data URIs) applied to the page.pageBackgroundImageOpacity— controls the image opacity (0–1) so the pattern lives beneath content without overwhelming it.boxBackgroundImage/boxBackgroundImageOpacity— the same controls scoped toesp-boxsurfaces.
Combined with CSS repeating-linear-gradient, repeating-conic-gradient, or tiled SVG patterns, this opens the door to rich textural identities — from subtle linen weaves to bold geometric tessellations.
The <esp-root> API
The <esp-root> component is the orchestrator. It accepts three key attributes:
scheme—"light"or"dark". Selects which resolved theme is active.light-theme— a Base64-encoded JSON string representing a partial theme for light mode. Only the fields you want to override need to be present; everything else inherits from the built-in defaults.dark-theme— the same, for dark mode.
Light and dark themes are independent. They can share the same seed and fonts, or they can diverge completely — different seeds, different angles, different fonts, different moods. A daytime theme might use warm terracotta with airy serifs; its nighttime counterpart might shift to deep indigo with tight geometric sans-serifs. They are two faces of the same identity.
Encoding a Theme
A partial theme is a plain JavaScript object. Encode it with btoa(JSON.stringify(partial)):
// A partial theme — only override what you need
const myTheme = {
seedColor: "oklch(0.65 0.20 30)",
fontBody: '"Lora", serif',
fontHeadings: '"Playfair Display", serif',
typeRatio: 1.2,
angles: { analogous: 40 },
};
const encoded = btoa(JSON.stringify(myTheme));
// Pass to <esp-root light-theme="...">
Living Showcase: Artistic Themes
The following demo lets you experience the theming engine in action. Each theme defines independent light and dark schemes — they don't merely invert; they transform mood, evoking day and night as distinct emotional states. Some use background tiles to add texture and depth. Select a theme from the picker, then hit Apply to transform the page. Hit Reset to return to the default documentation theme.
<esp-box>
<div style="display: grid; gap: var(--esp-size-padding); grid-template-columns: 1fr auto auto;">
<esp-form-item label="Choose a theme">
<esp-pick-one id="theme-picker">
<esp-picker-item text="Kyoto Garden" value="kyoto" selected></esp-picker-item>
<esp-picker-item text="Bauhaus Revival" value="bauhaus"></esp-picker-item>
<esp-picker-item text="Bioluminescent Abyss" value="abyss"></esp-picker-item>
<esp-picker-item text="Terracotta Manuscript" value="terracotta"></esp-picker-item>
<esp-picker-item text="Arctic Aurora" value="aurora"></esp-picker-item>
<esp-picker-item text="Retrowave Sunset" value="retrowave"></esp-picker-item>
</esp-pick-one>
</esp-form-item>
<esp-button id="apply-theme-btn" label="Apply" variant="primary" collapsed></esp-button>
<esp-button id="reset-theme-btn" label="Reset" collapsed></esp-button>
</div>
<div id="theme-description" style="padding: var(--esp-size-padding); border: 1px solid var(--esp-color-border); border-radius: var(--esp-size-border-radius); margin-top: var(--esp-size-small);"></div>
</esp-box>
<script>
const picker = findById("theme-picker");
const applyBtn = findById("apply-theme-btn");
const resetBtn = findById("reset-theme-btn");
const desc = findById("theme-description");
const themes = {
kyoto: {
name: "Kyoto Garden",
description: "Light: sun-dappled moss gardens under warm midday light, with a woven bamboo texture. Dark: moonlit stone pathways through indigo shadows — quiet, meditative, deep.",
light: {
seedColor: "oklch(0.68 0.16 145)",
fontBody: '"Noto Serif JP", serif',
fontHeadings: '"Cormorant Garamond", serif',
fontMonospace: '"Sometype Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.618,
borderRadius: 0.15,
angles: { analogous: 25, triadic: 110 },
lightness: {
surface: 0.97,
raised1: 0.94,
raised2: 0.88,
raised3: 0.78,
raised4: 0.70,
accent: 0.45,
muted: 0.40,
text: 0.30,
border: 0.50,
ink: 0.20,
shadow: 0.15
},
chroma: {
background: { min: 0.01, max: 0.04 },
layer1: { min: 0.01, max: 0.04 },
layer2: { min: 0.02, max: 0.06 },
text: { min: 0.02, max: 0.08 },
headings: { min: 0.06, max: 0.14 },
link: { min: 0.10, max: 0.20 },
border: { min: 0.03, max: 0.08 }
},
semanticMappings: {
headings: { source: "analogous-left", lightness: "muted" },
headingsHover: { source: "analogous-left", lightness: "text" },
link: { source: "triadic-right", lightness: "accent" },
linkHover: { source: "triadic-right", lightness: "text" },
linkHoverBg: { source: "analogous-right", lightness: "raised2" },
actionBackground: { source: "analogous-right", lightness: "raised3" },
actionText: { source: "analogous-right", lightness: "ink" },
inputCaret: { source: "complementary", lightness: "ink" },
inputSelection: { source: "complementary", lightness: "text" },
inputSelectionBg: { source: "complementary", lightness: "raised2" },
border: { source: "analogous-left", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(45deg, transparent, transparent 10px, oklch(0.90 0.03 145 / 0.3) 10px, oklch(0.90 0.03 145 / 0.3) 11px)",
pageBackgroundImageOpacity: 0.4
},
dark: {
seedColor: "oklch(0.45 0.10 260)",
fontBody: '"Noto Serif JP", serif',
fontHeadings: '"Cormorant Garamond", serif',
fontMonospace: '"Sometype Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.618,
borderRadius: 0.15,
angles: { analogous: 25, triadic: 110 },
lightness: {
surface: 0.12,
raised1: 0.16,
raised2: 0.22,
raised3: 0.32,
raised4: 0.28,
accent: 0.55,
muted: 0.70,
text: 0.88,
border: 0.35,
ink: 0.95,
shadow: 0.08
},
chroma: {
background: { min: 0.01, max: 0.04 },
layer1: { min: 0.01, max: 0.04 },
text: { min: 0.01, max: 0.05 },
headings: { min: 0.05, max: 0.12 },
link: { min: 0.10, max: 0.22 },
border: { min: 0.02, max: 0.06 }
},
semanticMappings: {
headings: { source: "analogous-right", lightness: "muted" },
headingsHover: { source: "analogous-right", lightness: "text" },
link: { source: "split-complementary-left", lightness: "accent" },
linkHover: { source: "split-complementary-left", lightness: "text" },
linkHoverBg: { source: "triadic-left", lightness: "raised2" },
actionBackground: { source: "triadic-left", lightness: "raised3" },
actionText: { source: "triadic-left", lightness: "ink" },
inputCaret: { source: "complementary", lightness: "ink" },
inputSelection: { source: "complementary", lightness: "text" },
inputSelectionBg: { source: "triadic-right", lightness: "raised2" },
border: { source: "analogous-left", lightness: "border" }
},
pageBackgroundImage: "radial-gradient(circle at 20% 50%, oklch(0.18 0.06 270 / 0.5) 0%, transparent 50%), radial-gradient(circle at 80% 20%, oklch(0.16 0.04 200 / 0.3) 0%, transparent 40%)",
pageBackgroundImageOpacity: 0.6
}
},
bauhaus: {
name: "Bauhaus Revival",
description: "Light: primary-color confidence with sharp geometry and bold grid energy — a repeating tile of intersecting lines. Dark: the same Bauhaus discipline distilled to neon wireframes on black, like architectural blueprints lit by studio lamps.",
light: {
seedColor: "oklch(0.60 0.24 30)",
fontBody: '"DM Sans", sans-serif',
fontHeadings: '"Bebas Neue", sans-serif',
fontMonospace: '"Space Mono", monospace',
typeRatio: 1.28,
spaceRatio: 1.5,
borderRadius: 0,
angles: { analogous: 60, complementary: 180, triadic: 120 },
lightness: {
surface: 0.98,
raised1: 0.95,
raised2: 0.88,
raised3: 0.75,
raised4: 0.65,
accent: 0.52,
muted: 0.42,
text: 0.25,
border: 0.30,
ink: 0.15,
shadow: 0.10
},
chroma: {
background: { min: 0.0, max: 0.02 },
layer1: { min: 0.0, max: 0.02 },
headings: { min: 0.15, max: 0.30 },
link: { min: 0.18, max: 0.35 },
actionBackground: { min: 0.12, max: 0.25 }
},
semanticMappings: {
headings: { source: "complementary", lightness: "muted" },
headingsHover: { source: "complementary", lightness: "text" },
link: { source: "triadic-left", lightness: "accent" },
linkHover: { source: "triadic-left", lightness: "text" },
linkHoverBg: { source: "triadic-right", lightness: "raised2" },
actionBackground: { source: "triadic-right", lightness: "raised3" },
actionText: { source: "triadic-right", lightness: "ink" },
inputCaret: { source: "triadic-left", lightness: "ink" },
inputSelection: { source: "triadic-left", lightness: "text" },
inputSelectionBg: { source: "triadic-right", lightness: "raised2" },
layer3: { source: "analogous-left", lightness: "raised3" },
layer4: { source: "analogous-right", lightness: "raised4" }
},
pageBackgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 39px, oklch(0.85 0.01 30 / 0.25) 39px, oklch(0.85 0.01 30 / 0.25) 40px), repeating-linear-gradient(90deg, transparent, transparent 39px, oklch(0.85 0.01 30 / 0.25) 39px, oklch(0.85 0.01 30 / 0.25) 40px)",
pageBackgroundImageOpacity: 0.5
},
dark: {
seedColor: "oklch(0.72 0.28 255)",
fontBody: '"DM Sans", sans-serif',
fontHeadings: '"Bebas Neue", sans-serif',
fontMonospace: '"Space Mono", monospace',
typeRatio: 1.28,
spaceRatio: 1.5,
borderRadius: 0,
angles: { analogous: 60, complementary: 180, triadic: 120 },
lightness: {
surface: 0.08,
raised1: 0.11,
raised2: 0.16,
raised3: 0.24,
raised4: 0.20,
accent: 0.70,
muted: 0.78,
text: 0.92,
border: 0.30,
ink: 0.97,
shadow: 0.04
},
chroma: {
background: { min: 0.0, max: 0.015 },
layer1: { min: 0.0, max: 0.015 },
headings: { min: 0.20, max: 0.35 },
link: { min: 0.22, max: 0.38 },
border: { min: 0.08, max: 0.18 }
},
semanticMappings: {
headings: { source: "triadic-right", lightness: "muted" },
headingsHover: { source: "triadic-right", lightness: "text" },
link: { source: "complementary", lightness: "accent" },
linkHover: { source: "complementary", lightness: "text" },
linkHoverBg: { source: "triadic-left", lightness: "raised2" },
actionBackground: { source: "analogous-left", lightness: "raised3" },
actionText: { source: "analogous-left", lightness: "ink" },
inputCaret: { source: "complementary", lightness: "ink" },
inputSelection: { source: "complementary", lightness: "text" },
inputSelectionBg: { source: "analogous-right", lightness: "raised2" },
layer3: { source: "triadic-left", lightness: "raised3" },
layer4: { source: "triadic-right", lightness: "raised4" },
border: { source: "triadic-left", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 39px, oklch(0.25 0.08 255 / 0.2) 39px, oklch(0.25 0.08 255 / 0.2) 40px), repeating-linear-gradient(90deg, transparent, transparent 39px, oklch(0.25 0.08 255 / 0.2) 39px, oklch(0.25 0.08 255 / 0.2) 40px)",
pageBackgroundImageOpacity: 0.4
}
},
abyss: {
name: "Bioluminescent Abyss",
description: "Light: a shallow tropical lagoon — crystalline cyan surfaces with soft coral accents and rippling caustic patterns. Dark: the deep ocean floor, where bioluminescent organisms pulse in electric cyan against crushing darkness.",
light: {
seedColor: "oklch(0.72 0.14 195)",
fontBody: '"Nunito", sans-serif',
fontHeadings: '"Righteous", cursive',
fontMonospace: '"Fira Code", monospace',
typeRatio: 1.22,
spaceRatio: 1.5,
borderRadius: 0.5,
angles: { analogous: 35, complementary: 180, splitComplementary: 40, triadic: 130 },
lightness: {
surface: 0.97,
raised1: 0.94,
raised2: 0.88,
raised3: 0.78,
raised4: 0.70,
accent: 0.48,
muted: 0.42,
text: 0.30,
border: 0.60,
ink: 0.22,
shadow: 0.18
},
chroma: {
background: { min: 0.02, max: 0.05 },
layer1: { min: 0.02, max: 0.05 },
layer2: { min: 0.03, max: 0.07 },
headings: { min: 0.08, max: 0.18 },
link: { min: 0.12, max: 0.22 },
border: { min: 0.04, max: 0.10 }
},
semanticMappings: {
headings: { source: "split-complementary-right", lightness: "muted" },
headingsHover: { source: "split-complementary-right", lightness: "text" },
link: { source: "complementary", lightness: "accent" },
linkHover: { source: "complementary", lightness: "text" },
linkHoverBg: { source: "split-complementary-left", lightness: "raised2" },
actionBackground: { source: "triadic-left", lightness: "raised3" },
actionText: { source: "triadic-left", lightness: "ink" },
inputCaret: { source: "analogous-right", lightness: "ink" },
inputSelection: { source: "analogous-right", lightness: "text" },
inputSelectionBg: { source: "analogous-right", lightness: "raised2" },
layer2: { source: "analogous-left", lightness: "raised2" },
border: { source: "split-complementary-left", lightness: "border" }
},
pageBackgroundImage: "repeating-conic-gradient(from 0deg at 50% 50%, oklch(0.94 0.04 195 / 0.15) 0deg, transparent 15deg, oklch(0.94 0.04 210 / 0.10) 30deg, transparent 45deg)",
pageBackgroundImageOpacity: 0.5
},
dark: {
seedColor: "oklch(0.65 0.22 190)",
fontBody: '"Nunito", sans-serif',
fontHeadings: '"Righteous", cursive',
fontMonospace: '"Fira Code", monospace',
typeRatio: 1.22,
spaceRatio: 1.5,
borderRadius: 0.5,
angles: { analogous: 35, complementary: 180, splitComplementary: 40, triadic: 130 },
lightness: {
surface: 0.06,
raised1: 0.09,
raised2: 0.14,
raised3: 0.22,
raised4: 0.18,
accent: 0.65,
muted: 0.72,
text: 0.90,
border: 0.28,
ink: 0.97,
shadow: 0.03
},
chroma: {
background: { min: 0.01, max: 0.04 },
layer1: { min: 0.02, max: 0.06 },
headings: { min: 0.14, max: 0.28 },
link: { min: 0.18, max: 0.32 },
border: { min: 0.06, max: 0.14 }
},
semanticMappings: {
headings: { source: "triadic-right", lightness: "muted" },
headingsHover: { source: "triadic-right", lightness: "text" },
link: { source: "split-complementary-left", lightness: "accent" },
linkHover: { source: "split-complementary-left", lightness: "text" },
linkHoverBg: { source: "analogous-left", lightness: "raised2" },
actionBackground: { source: "complementary", lightness: "raised3" },
actionText: { source: "complementary", lightness: "ink" },
inputCaret: { source: "triadic-left", lightness: "ink" },
inputSelection: { source: "triadic-left", lightness: "text" },
inputSelectionBg: { source: "triadic-left", lightness: "raised2" },
layer2: { source: "analogous-right", lightness: "raised2" },
layer3: { source: "split-complementary-right", lightness: "raised3" },
border: { source: "analogous-left", lightness: "border" }
},
pageBackgroundImage: "radial-gradient(ellipse at 30% 70%, oklch(0.15 0.12 190 / 0.4) 0%, transparent 50%), radial-gradient(ellipse at 70% 30%, oklch(0.12 0.10 160 / 0.3) 0%, transparent 45%), radial-gradient(ellipse at 50% 50%, oklch(0.10 0.08 220 / 0.2) 0%, transparent 60%)",
pageBackgroundImageOpacity: 0.7
}
},
terracotta: {
name: "Terracotta Manuscript",
description: "Light: sun-baked clay walls and hand-lettered parchment — a diagonal crosshatch like aged linen paper. Dark: firelit library at midnight, where amber candlelight dances across leather-bound spines and warm wood grain.",
light: {
seedColor: "oklch(0.62 0.13 55)",
fontBody: '"Lora", serif',
fontHeadings: '"Playfair Display", serif',
fontMonospace: '"IBM Plex Mono", monospace',
typeRatio: 1.20,
spaceRatio: 1.618,
borderRadius: 0.1,
angles: { analogous: 20, complementary: 180, triadic: 100 },
semanticHues: { danger: 25, success: 145, warning: 80 },
lightness: {
surface: 0.96,
raised1: 0.93,
raised2: 0.87,
raised3: 0.76,
raised4: 0.68,
accent: 0.48,
muted: 0.40,
text: 0.30,
border: 0.55,
ink: 0.22,
shadow: 0.16
},
chroma: {
background: { min: 0.02, max: 0.06 },
layer1: { min: 0.02, max: 0.06 },
layer2: { min: 0.03, max: 0.07 },
text: { min: 0.02, max: 0.06 },
headings: { min: 0.06, max: 0.12 },
link: { min: 0.08, max: 0.16 },
border: { min: 0.04, max: 0.09 }
},
semanticMappings: {
headings: { source: "analogous-left", lightness: "muted" },
headingsHover: { source: "analogous-left", lightness: "text" },
link: { source: "complementary", lightness: "accent" },
linkHover: { source: "complementary", lightness: "text" },
linkHoverBg: { source: "analogous-right", lightness: "raised2" },
actionBackground: { source: "complementary", lightness: "raised3" },
actionText: { source: "complementary", lightness: "ink" },
inputCaret: { source: "triadic-left", lightness: "ink" },
inputSelection: { source: "triadic-left", lightness: "text" },
inputSelectionBg: { source: "triadic-left", lightness: "raised2" },
layer2: { source: "analogous-right", lightness: "raised2" },
border: { source: "analogous-left", lightness: "border" },
shadow: { source: "analogous-left", lightness: "shadow" }
},
pageBackgroundImage: "repeating-linear-gradient(45deg, transparent, transparent 7px, oklch(0.88 0.04 55 / 0.2) 7px, oklch(0.88 0.04 55 / 0.2) 8px), repeating-linear-gradient(-45deg, transparent, transparent 7px, oklch(0.88 0.04 55 / 0.2) 7px, oklch(0.88 0.04 55 / 0.2) 8px)",
pageBackgroundImageOpacity: 0.35
},
dark: {
seedColor: "oklch(0.52 0.11 45)",
fontBody: '"Lora", serif',
fontHeadings: '"Playfair Display", serif',
fontMonospace: '"IBM Plex Mono", monospace',
typeRatio: 1.20,
spaceRatio: 1.618,
borderRadius: 0.1,
angles: { analogous: 20, complementary: 180, triadic: 100 },
semanticHues: { danger: 25, success: 145, warning: 80 },
lightness: {
surface: 0.13,
raised1: 0.17,
raised2: 0.24,
raised3: 0.34,
raised4: 0.30,
accent: 0.58,
muted: 0.68,
text: 0.86,
border: 0.38,
ink: 0.93,
shadow: 0.08
},
chroma: {
background: { min: 0.02, max: 0.05 },
layer1: { min: 0.02, max: 0.06 },
text: { min: 0.01, max: 0.04 },
headings: { min: 0.05, max: 0.10 },
link: { min: 0.10, max: 0.18 },
border: { min: 0.04, max: 0.08 }
},
semanticMappings: {
headings: { source: "analogous-right", lightness: "muted" },
headingsHover: { source: "analogous-right", lightness: "text" },
link: { source: "triadic-left", lightness: "accent" },
linkHover: { source: "triadic-left", lightness: "text" },
linkHoverBg: { source: "complementary", lightness: "raised2" },
actionBackground: { source: "analogous-left", lightness: "raised3" },
actionText: { source: "analogous-left", lightness: "ink" },
inputCaret: { source: "complementary", lightness: "ink" },
inputSelection: { source: "complementary", lightness: "text" },
inputSelectionBg: { source: "complementary", lightness: "raised2" },
layer2: { source: "analogous-left", lightness: "raised2" },
border: { source: "analogous-right", lightness: "border" },
shadow: { source: "analogous-right", lightness: "shadow" }
},
pageBackgroundImage: "repeating-linear-gradient(135deg, transparent, transparent 14px, oklch(0.20 0.05 45 / 0.15) 14px, oklch(0.20 0.05 45 / 0.15) 15px)",
pageBackgroundImageOpacity: 0.3
}
},
aurora: {
name: "Arctic Aurora",
description: "Light: crystalline arctic morning — ice-blue surfaces with prismatic rainbow refractions dancing across snow. Dark: the full aurora borealis — sweeping curtains of green and violet light rippling across a polar sky.",
light: {
seedColor: "oklch(0.70 0.12 230)",
fontBody: '"Inter", sans-serif',
fontHeadings: '"Outfit", sans-serif',
fontMonospace: '"JetBrains Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.5,
borderRadius: 0.35,
angles: { analogous: 45, complementary: 180, splitComplementary: 35, triadic: 120 },
lightness: {
surface: 0.98,
raised1: 0.96,
raised2: 0.91,
raised3: 0.82,
raised4: 0.74,
accent: 0.50,
muted: 0.44,
text: 0.28,
border: 0.65,
ink: 0.20,
shadow: 0.15
},
chroma: {
background: { min: 0.01, max: 0.03 },
layer1: { min: 0.01, max: 0.04 },
headings: { min: 0.08, max: 0.16 },
link: { min: 0.12, max: 0.24 },
border: { min: 0.02, max: 0.06 }
},
semanticMappings: {
headings: { source: "split-complementary-left", lightness: "muted" },
headingsHover: { source: "split-complementary-left", lightness: "text" },
link: { source: "triadic-right", lightness: "accent" },
linkHover: { source: "triadic-right", lightness: "text" },
linkHoverBg: { source: "analogous-left", lightness: "raised2" },
actionBackground: { source: "split-complementary-right", lightness: "raised3" },
actionText: { source: "split-complementary-right", lightness: "ink" },
inputCaret: { source: "triadic-left", lightness: "ink" },
inputSelection: { source: "triadic-left", lightness: "text" },
inputSelectionBg: { source: "triadic-left", lightness: "raised2" },
border: { source: "analogous-right", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(105deg, oklch(0.96 0.03 230 / 0.2) 0px, oklch(0.96 0.03 280 / 0.15) 40px, oklch(0.96 0.03 330 / 0.1) 80px, oklch(0.96 0.03 150 / 0.15) 120px, oklch(0.96 0.03 230 / 0.2) 160px)",
pageBackgroundImageOpacity: 0.5
},
dark: {
seedColor: "oklch(0.62 0.20 155)",
fontBody: '"Inter", sans-serif',
fontHeadings: '"Outfit", sans-serif',
fontMonospace: '"JetBrains Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.5,
borderRadius: 0.35,
angles: { analogous: 50, complementary: 170, splitComplementary: 35, triadic: 120 },
lightness: {
surface: 0.07,
raised1: 0.10,
raised2: 0.16,
raised3: 0.25,
raised4: 0.21,
accent: 0.62,
muted: 0.72,
text: 0.90,
border: 0.30,
ink: 0.96,
shadow: 0.04
},
chroma: {
background: { min: 0.01, max: 0.03 },
layer1: { min: 0.02, max: 0.05 },
headings: { min: 0.12, max: 0.25 },
link: { min: 0.16, max: 0.30 },
border: { min: 0.05, max: 0.12 }
},
semanticMappings: {
headings: { source: "triadic-left", lightness: "muted" },
headingsHover: { source: "triadic-left", lightness: "text" },
link: { source: "split-complementary-right", lightness: "accent" },
linkHover: { source: "split-complementary-right", lightness: "text" },
linkHoverBg: { source: "analogous-right", lightness: "raised2" },
actionBackground: { source: "triadic-right", lightness: "raised3" },
actionText: { source: "triadic-right", lightness: "ink" },
inputCaret: { source: "complementary", lightness: "ink" },
inputSelection: { source: "complementary", lightness: "text" },
inputSelectionBg: { source: "complementary", lightness: "raised2" },
layer2: { source: "analogous-left", lightness: "raised2" },
border: { source: "triadic-left", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(170deg, oklch(0.12 0.08 155 / 0.3) 0px, oklch(0.10 0.06 180 / 0.2) 60px, oklch(0.08 0.10 280 / 0.25) 120px, oklch(0.10 0.06 155 / 0.2) 180px, oklch(0.12 0.08 155 / 0.3) 240px)",
pageBackgroundImageOpacity: 0.6
}
},
retrowave: {
name: "Retrowave Sunset",
description: "Light: Miami Vice pastel paradise — warm pinks and peach over a soft diagonal stripe like venetian blinds catching afternoon sun. Dark: full synthwave — hot magenta and electric cyan slicing through a midnight chrome horizon with scan lines.",
light: {
seedColor: "oklch(0.70 0.18 350)",
fontBody: '"Exo 2", sans-serif',
fontHeadings: '"Orbitron", sans-serif',
fontMonospace: '"Share Tech Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.5,
borderRadius: 0.25,
angles: { analogous: 30, complementary: 165, splitComplementary: 25, triadic: 115 },
lightness: {
surface: 0.97,
raised1: 0.94,
raised2: 0.88,
raised3: 0.78,
raised4: 0.70,
accent: 0.52,
muted: 0.44,
text: 0.30,
border: 0.60,
ink: 0.22,
shadow: 0.16
},
chroma: {
background: { min: 0.02, max: 0.05 },
layer1: { min: 0.02, max: 0.05 },
headings: { min: 0.12, max: 0.22 },
link: { min: 0.14, max: 0.28 },
border: { min: 0.04, max: 0.10 }
},
semanticMappings: {
headings: { source: "complementary", lightness: "muted" },
headingsHover: { source: "complementary", lightness: "text" },
link: { source: "triadic-left", lightness: "accent" },
linkHover: { source: "triadic-left", lightness: "text" },
linkHoverBg: { source: "analogous-right", lightness: "raised2" },
actionBackground: { source: "analogous-left", lightness: "raised3" },
actionText: { source: "analogous-left", lightness: "ink" },
inputCaret: { source: "split-complementary-right", lightness: "ink" },
inputSelection: { source: "split-complementary-right", lightness: "text" },
inputSelectionBg: { source: "split-complementary-right", lightness: "raised2" },
border: { source: "analogous-right", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(135deg, transparent, transparent 19px, oklch(0.92 0.06 350 / 0.15) 19px, oklch(0.92 0.06 350 / 0.15) 20px, transparent 20px, transparent 23px)",
pageBackgroundImageOpacity: 0.45
},
dark: {
seedColor: "oklch(0.60 0.25 330)",
fontBody: '"Exo 2", sans-serif',
fontHeadings: '"Orbitron", sans-serif',
fontMonospace: '"Share Tech Mono", monospace',
typeRatio: 1.25,
spaceRatio: 1.5,
borderRadius: 0.25,
angles: { analogous: 35, complementary: 160, splitComplementary: 30, triadic: 115 },
lightness: {
surface: 0.06,
raised1: 0.09,
raised2: 0.14,
raised3: 0.22,
raised4: 0.18,
accent: 0.68,
muted: 0.76,
text: 0.92,
border: 0.32,
ink: 0.97,
shadow: 0.03
},
chroma: {
background: { min: 0.01, max: 0.03 },
layer1: { min: 0.02, max: 0.05 },
headings: { min: 0.18, max: 0.32 },
link: { min: 0.20, max: 0.36 },
border: { min: 0.08, max: 0.16 }
},
semanticMappings: {
headings: { source: "triadic-right", lightness: "muted" },
headingsHover: { source: "triadic-right", lightness: "text" },
link: { source: "complementary", lightness: "accent" },
linkHover: { source: "complementary", lightness: "text" },
linkHoverBg: { source: "split-complementary-left", lightness: "raised2" },
actionBackground: { source: "triadic-left", lightness: "raised3" },
actionText: { source: "triadic-left", lightness: "ink" },
inputCaret: { source: "split-complementary-right", lightness: "ink" },
inputSelection: { source: "split-complementary-right", lightness: "text" },
inputSelectionBg: { source: "split-complementary-right", lightness: "raised2" },
layer2: { source: "analogous-left", lightness: "raised2" },
border: { source: "split-complementary-left", lightness: "border" }
},
pageBackgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 3px, oklch(0.15 0.10 330 / 0.12) 3px, oklch(0.15 0.10 330 / 0.12) 4px)",
pageBackgroundImageOpacity: 0.5
}
}
};
function updateDescription(value) {
const theme = themes[value];
if (theme && desc) {
desc.innerHTML = "<strong>" + theme.name + ":</strong> " + theme.description;
}
}
updateDescription("kyoto");
picker.addEventListener("value-changed", (ev) => {
if (ev.detail && ev.detail.value) {
updateDescription(ev.detail.value);
}
});
applyBtn.addEventListener("clicked", () => {
const value = picker.value;
const theme = themes[value];
if (!theme) return;
const lightEncoded = btoa(JSON.stringify(theme.light));
const darkEncoded = btoa(JSON.stringify(theme.dark));
localStorage.setItem("esp-light-theme", lightEncoded);
localStorage.setItem("esp-dark-theme", darkEncoded);
location.reload();
});
resetBtn.addEventListener("clicked", () => {
const docsDefault = {
fontBody: '"Quicksand", sans-serif',
fontHeadings: '"Oswald", sans-serif',
fontMonospace: '"Sometype Mono", monospace'
};
const encoded = btoa(JSON.stringify(docsDefault));
localStorage.setItem("esp-light-theme", encoded);
localStorage.setItem("esp-dark-theme", encoded);
location.reload();
});
</script>
Theme Anatomy: What Each Showcase Demonstrates
Kyoto Garden
The light scheme uses a green seed (oklch(0.68 0.16 145)) with tight chroma clamping on surfaces — backgrounds stay nearly neutral, letting content breathe against a subtle diagonal bamboo-weave pattern. Headings draw from analogous-left for a warm yellow-green that reads like sunlit foliage, while links pull from triadic-right for a contrasting plum that catches the eye without shouting. Input carets and selections route through complementary — a brief flash of red-violet, like a koi glimpsed beneath lily pads.
The dark scheme shifts the seed entirely to deep indigo (oklch(0.45 0.10 260)), evoking moonlit stone gardens. Here, headings move to analogous-right for a cooler blue-violet, and links shift to split-complementary-left — a muted gold that glows like lantern light. Actions route through triadic-left for subtle warmth against the cold backdrop. The font pairing — Noto Serif JP for body, Cormorant Garamond for headings — creates a calm, literary tone across both schemes.
Bauhaus Revival
Zero border-radius. Wide analogous angle (60°) for maximum geometric spread. The light scheme seeds from a bold red-orange and overlays a strict grid tile — every 40px, thin lines cross to create graph paper. The semantic mappings are deliberately primary-color: headings route through complementary (teal-blue) for Mondrian-like opposition, links through triadic-left (yellow), and actions through triadic-right (violet). Even the elevation layers (layer3, layer4) pull from left and right analogous variants, painting the UI in a full geometric triad.
The dark scheme pivots to electric blue as the seed and redistributes the color wheel: headings now pull from triadic-right, links from complementary, actions from analogous-left, and borders from triadic-left — every major UI element lives on a different spoke. The typography — Bebas Neue headings over DM Sans body — channels mid-century modernist confidence.
Bioluminescent Abyss
The light scheme is a tropical lagoon: cyan seed, rounded corners (0.5rem), and a conic gradient creating subtle caustic light patterns. Headings route through split-complementary-right for a warm coral tone (like sun-bleached coral reef), while links stay on complementary for a sandy warmth. Actions pull from triadic-left, borders from split-complementary-left, and the second elevation layer draws from analogous-left — creating a layered aquatic palette where every surface hints at a different depth of water.
The dark scheme drops to the ocean floor — the surface lightness plunges to 0.06, and radial gradients simulate bioluminescent organisms pulsing in the deep. The mappings shift to maximize the glow effect: headings route through triadic-right, links through split-complementary-left, and actions through complementary. Layers 2 and 3 pull from analogous-right and split-complementary-right, creating visible color shifts between elevation depths — like bioluminescent organisms at different ocean strata.
Terracotta Manuscript
A study in warmth and restraint. The light scheme uses a terracotta seed at hue 55° with a crosshatch linen texture. Tight chroma clamps everywhere keep surfaces earthy and desaturated. Headings draw from analogous-left for a slightly cooler clay tone; links route through complementary for a dusty blue-gray that evokes aged ink. The second elevation layer pulls from analogous-right for a warmer parchment tint, and borders and shadows both map through analogous-left to maintain a cohesive, handcrafted feel.
The dark scheme warms to candlelit amber — the seed shifts to hue 45°, and a single diagonal line pattern suggests aged wood grain. Headings move to analogous-right, links to triadic-left, and actions to analogous-left — subtle rotations that keep the warm monochrome mood but introduce just enough hue variation to feel alive by firelight. The serif pairing (Lora body, Playfair Display headings) anchors the scholarly mood.
Arctic Aurora
The light scheme seeds from icy blue (hue 230°) with a prismatic rainbow gradient — cycling through multiple hue-tinted bands at low opacity. Headings route through split-complementary-left for a warm amber that contrasts the icy background like morning sun on snow. Links pull from triadic-right, actions from split-complementary-right, and inputs from triadic-left — scattering color across the spectrum like light through an ice prism.
The dark scheme transforms entirely to aurora green (hue 155°) with sweeping multi-hue curtains. The semantic mappings mirror the aurora's color play: headings draw from triadic-left (violet), links from split-complementary-right (warm amber), actions from triadic-right, and borders from triadic-left. The wider analogous angle (50°) and off-center complementary (170°) create an ethereal, otherworldly color geometry where every UI element feels like a different band of the aurora.
Retrowave Sunset
Light mode: pastel pink paradise with venetian-blind diagonal stripes. Headings route through complementary for a cool teal that pops against the warm pink seed — classic Miami Vice color blocking. Links draw from triadic-left, actions from analogous-left, and inputs from split-complementary-right, distributing pastel variation across the interface.
Dark mode: full synthwave — the seed jumps to hot magenta (hue 330°) with scan lines — a 4px repeating horizontal stripe that channels CRT monitors. The mappings shift for maximum neon impact: headings pull from triadic-right (electric cyan), links from complementary (green), and actions from triadic-left. The second elevation layer routes through analogous-left, and borders through split-complementary-left — every element glows in a different neon frequency. Orbitron headings and Share Tech Mono code blocks complete the retro-futurist identity.
Building Your Own Theme
Start small. Override only what matters:
// Minimal: just change the seed color
const minimal = {
seedColor: "oklch(0.65 0.18 285)"
};
// Moderate: seed + fonts + weights + tighter surfaces
const moderate = {
seedColor: "oklch(0.58 0.14 170)",
fontBody: '"Source Sans 3", sans-serif',
fontWeightBody: "300",
fontHeadings: '"Source Serif 4", serif',
fontWeightHeadings: "700",
chroma: {
background: { min: 0, max: 0.03 },
layer1: { min: 0, max: 0.03 }
}
};
// Advanced: independent variant chroma for vivid accents
const advanced = {
seedColor: "oklch(0.65 0.12 216)",
variantChroma: {
"complementary": 0.24,
"triadic-left": 0.18,
"triadic-right": 0.18,
"danger": 0.28,
"success": 0.10,
"warning": 0.10,
}
};
// Full control: independent light and dark
const lightTheme = btoa(JSON.stringify({
seedColor: "oklch(0.70 0.16 45)",
fontHeadings: '"Abril Fatface", serif',
fontWeightHeadings: "900",
typeRatio: 1.2
}));
const darkTheme = btoa(JSON.stringify({
seedColor: "oklch(0.55 0.20 270)",
fontHeadings: '"Space Grotesk", sans-serif',
fontWeightHeadings: "600",
typeRatio: 1.25
}));
Pass the encoded strings to <esp-root>:
<esp-root
scheme="light"
light-theme="eyJzZWVkQ29sb3IiOiAib2tsY2goMC43IDAuMTYgNDUpIn0="
dark-theme="eyJzZWVkQ29sb3IiOiAib2tsY2goMC41NSAwLjIwIDI3MCkifQ=="
>
<!-- Your entire application -->
</esp-root>
The engine handles the rest — computing variants, enforcing APCA contrast, generating fluid scales, and mapping every semantic token to its final CSS custom property.