Theming
A theme is a point in an eight-channel space. The engine derives every token from it, so radius, depth, motion and type change together and stay coherent.
Apply a theme
Two attributes do all of it. data-theme picks the character and data-mode picks light or dark. Without either, you get graphite in light mode.
<!-- The whole document -->
<html data-theme="graphite" data-mode="dark">
<!-- Or any subtree: themes nest -->
<section data-theme="atelier" data-mode="light">…</section>Canonical themes
- GraphiteDefault
data-theme="graphite"Neutral graphite, crisp hairlines, and ink that stays out of the way.
- Atelier Umami
data-theme="atelier"Warm cotton paper, artisanal ink, and an amber signal.
- Machined Titanium
data-theme="machined"Aerospace telemetry, compact precision, and high contrast.
- Aerogel Glass
data-theme="aerogel"Optical luminescence, liquid refraction, and specular depth.
- Nordic Earth
data-theme="nordic"Forest moss, fjord stone, terracotta, and organic calm.
The eight channels
Each channel is a number from 0 to 1. Moving one changes several tokens at once.
| Channel | What it moves |
|---|---|
type | utilitarian ↔ editorial: display weight, tracking, leading |
geometry | rectilinear ↔ organic: radius and border weight |
density | compact ↔ spacious: body line height |
depth | flat ↔ layered: shadow offset, blur and strength |
motion | still ↔ kinetic: durations and spring damping |
texture | polished ↔ tactile: grain and border presence |
rhythm | regular ↔ syncopated: tracking, timing and leading couplings |
icon | systematic ↔ expressive: glyph stroke weight |
Your own theme
Write mlola.theme.json at the project root. Start from a canonical theme and change only what differs; every key is optional, and unknown keys are ignored so a theme written today keeps working as the engine grows.
{
"$schema": "./packages/engine/generated/theme-spec.schema.json",
"id": "acme",
"label": "Acme",
"//": "A theme is a few decisions. The engine derives every token and solves contrast in both modes. Every key is optional.",
"inherit": "nordic",
"//vector": "Eight channels from 0 to 1. Each one moves geometry, rhythm, depth and motion together.",
"vector": {
"type": 0.7,
"geometry": 0.35,
"density": 0.45,
"depth": 0.3,
"motion": 0.4,
"texture": 0.2,
"rhythm": 0.5,
"icon": 0.6
},
"//color": "The primary seed keeps its hue and chroma; its lightness is adjusted only as far as contrast demands.",
"color": {
"primary": "oklch(0.55 0.19 265)",
"primaryDark": "oklch(0.72 0.17 265)",
"neutral": {
"hue": 265,
"chroma": 0.006
}
},
"//material": "solid, glass, paper or anodized. Applied to every surface.",
"material": "solid",
"//fonts": "A named set (neutral, editorial, technical, humanist, geometric) or explicit stacks.",
"fonts": {
"sans": "\"Inter Tight\", ui-sans-serif, system-ui, sans-serif",
"display": "\"Fraunces\", Georgia, serif",
"mono": "\"JetBrains Mono\", ui-monospace, monospace"
},
"//scale": "Override any step of the spacing or type scale without touching the rest.",
"scale": {
"space-4": "1.0625rem",
"type-base": "0.9375rem"
},
"//extend": "Raw custom properties. The escape hatch for anything the engine has not modelled yet.",
"extend": {
"--acme-brand-gradient": "linear-gradient(120deg, oklch(0.55 0.19 265), oklch(0.72 0.17 200))"
}
}The engine compiles it next to the canonical themes, on the same data-theme contract, so nothing about using it is special.