Skip to content

Theming and presets

Open .md

The whole look comes from one tokens file, tokens/default.json. A build step turns it into CSS custom properties (tokens.css) and a Tailwind v4 theme (theme.css). At runtime nothing is computed in JavaScript: five attributes only remap CSS variables.

Attribute Values Default (no attribute)
data-theme light, dark follows prefers-color-scheme
data-accent durazno, menta, cielo, lavanda, orquidea, grafito Coral
data-style soft, bordered, elevated, flat, glass, brutal soft
data-density compact (x0.875), normal, comfortable (x1.125) normal
data-radius none, sm, md, lg, full the preset’s own radii
<html data-theme="dark" data-style="brutal" data-accent="cielo" data-density="comfortable">
...
<section data-style="glass">only this subtree is glass</section>
</html>

Each preset changes shape, edge and elevation, not the palette:

  • soft: hairline shadows instead of borders, pill buttons (the default).
  • bordered: crisp 1px borders, no shadows, tighter radii.
  • elevated: real shadows and a lifted card surface; has light and dark variants.
  • flat: no shadows and no borders; surfaces separate by tone only.
  • glass: translucent surfaces with backdrop blur; has light and dark variants.
  • brutal: thick borders, hard offset shadows, square corners.

On a single element the order is source order at equal specificity: theme, then accent, then data-style, then data-density, then data-radius. data-radius also wins over a preset set on a descendant, because presets read var(--radius-card-override, <own radius>). Presets never touch density tokens. There is no !important anywhere.

Density is a unitless --density that scales control heights, control and card padding and the gap-density* scale with calc(). Tailwind’s global --spacing scale is not scaled.

Use the semantic utilities so your UI follows the presets too:

Concern Utilities
Radius rounded-button, rounded-input, rounded-card, rounded-popover, rounded-chip…
Edge border-card, border-control
Shadow shadow-card, shadow-raised, shadow-overlay, shadow-control
Surface bg-surface-card, bg-surface-control, bg-surface-overlay, bg-surface-sunken
Density h-control, h-input, px-control-x, p-card, gap-density
Focus and motion focus-visible:focus-ring, pressable, ease-style

The complete list of tokens, with light and dark values and usage notes, is generated on the Design tokens page.

Copy tokens/default.json, edit values (keep the same names) and run the CLI:

Terminal window
npx ui-theme build my-tokens.json --out src/theme

It writes tokens.css, theme.css, fonts.css and a Compose Theme.kt so a Kotlin app can share the tokens. Import your generated files instead of the shipped ones.

const root = document.documentElement;
root.dataset.style = "glass";
root.dataset.accent = "menta";
localStorage.setItem("preset", JSON.stringify({ style: "glass", accent: "menta" }));

Restore it from an inline script in <head> before first paint to avoid a flash. This site does exactly that.