Theming

Change the base theme's tokens, or add a theme of your own.

A theme is a set of token values. Components read the tokens and never a fixed colour, so restyling the whole UI is changing a few custom properties. Nothing is compiled per theme.

Tokens

Each colour comes as a pair: the fill and the text that sits on it. The swatches below read the live tokens, so they follow the theme and light or dark.

Aa
background foreground The page and its default text.
Aa
card card-foreground A card.
Aa
primary primary-foreground The default button and badge.
Aa
secondary secondary-foreground The secondary variant.
Aa
muted muted-foreground Quiet fills and quiet text.
Aa
accent accent-foreground The hover fill of outline and ghost buttons.
Aa
destructive destructive-foreground The destructive variant of buttons, badges and alerts.
Aa
success success-foreground The success variant of badges and alerts.
Aa
warning warning-foreground The warning variant of badges and alerts.
Aa
info info-foreground The info variant of badges and alerts.

Four more tokens set the edges and the shape.

border Every border.
input The border of a field.
ring The focus ring.
radius The base corner radius. Smaller parts use a little less of it.

Colour values are HSL channels without the hsl(): --primary: 221 83% 53%. Pick each foreground to read clearly on its fill.

Changing the base theme

The preset writes the tokens first and the component CSS after them. A site's own rules go after both, in the same ui layer, so they win without raising specificity. Put them in a CSS file and add it to the site's UnoCSS config as a preflight:

import { readFileSync } from 'node:fs';
import { defineConfig } from 'unocss';
import ui from '../../phlat/addons/ui/assets/preset.mjs';

export default defineConfig({
	presets: [ui()],
	preflights: [
		{
			layer: 'ui',
			getCSS: () => readFileSync(new URL('./theme.css', import.meta.url), 'utf8'),
		},
	],
});

Then set the tokens to change in theme.css:

:root {
	--primary: 221 83% 53%;
	--primary-foreground: 0 0% 98%;
	--ring: 221 83% 53%;
	--radius: 0.25rem;
}

Those values are the light theme. The preset writes its dark values for [data-theme=dark] and, inside prefers-color-scheme: dark, for :root:not([data-theme=light]), which outweighs :root, so a light override never reaches dark. To change dark, write the same two selectors:

[data-theme=dark] {
	--primary: 217 91% 70%;
}

@media (prefers-color-scheme: dark) {
	:root:not([data-theme=light]) {
		--primary: 217 91% 70%;
	}
}

To restyle one component, set its own variables instead, such as .btn { --btn-h: 3rem; }. The overview covers how components read them.

Adding a theme

A theme is a selector that sets every token, picked by a data-theme attribute. Set all of them: a token left out falls through to the light or dark value beneath it. Add color-scheme so native controls and scrollbars match, and avoid the names light and dark, which the preset already uses.

:root[data-theme=forest],
[data-theme=forest] {
	--background: 60 20% 98%;
	--foreground: 150 20% 8%;
	--card: 0 0% 100%;
	--card-foreground: 150 20% 8%;
	--primary: 152 55% 26%;
	--primary-foreground: 60 20% 98%;
	--secondary: 140 25% 90%;
	--secondary-foreground: 152 55% 16%;
	--muted: 140 15% 94%;
	--muted-foreground: 150 8% 40%;
	--accent: 140 25% 90%;
	--accent-foreground: 152 55% 16%;
	--destructive: 0 72% 45%;
	--destructive-foreground: 0 0% 98%;
	--border: 140 15% 85%;
	--input: 140 15% 85%;
	--ring: 152 55% 26%;
	--radius: 0.75rem;
	color-scheme: light;
}

The selector has two parts for a reason. :root[data-theme=forest] is what <html data-theme="forest"> matches, and it needs the extra weight to beat the preset's system dark rule. On any other element [data-theme=forest] is enough.

Using a theme

Set the attribute on <html> to theme the page, from wherever the site keeps the choice:

<html
	lang="en"
	data-theme="{$theme}"
>

Without the attribute the page follows the system, light or dark.

On any other element the attribute themes just that region. Tokens inherit, but only the body paints a background, so give the region its own:

<section
	data-theme="forest"
	class="bg-background text-foreground p-6 rounded-lg"
>
	{ui button, title: 'Save' /}
</section>

Components inside read the region's tokens, so the same button is forest green here and the base theme everywhere else.