Theming

Havelcss ships greys and one placeholder accent. Everything else you decide, and most themes are a dozen lines.

Colors come in pairs

Every color exists twice, once per scheme, and light-dark() picks between them. Set the pair and both schemes follow:

:root {
	--lightmode-color-brand: oklch(0.62 0.11 220);
	--darkmode-color-brand: oklch(0.72 0.13 220);
}

Setting only one side is fine, the other keeps its default. These two are inputs and nothing else. In your own rules always read the effective token instead, so the value follows whichever scheme is actually active:

.badge {
	background: var(--color-bg-elevated);
	color: var(--color-text-muted);
}

The color tokens

Ten roles, each with a --lightmode- and a --darkmode- input behind it. The swatches show the scheme you are in right now.

Token What it is
--color-bg The page
--color-bg-surface One step off the page: code blocks, progress tracks, readonly fields
--color-bg-elevated Sitting above the page: details, popover, dialog, tooltip, cards
--color-text Body text
--color-text-strong Emphasis, and what hover and focus move towards
--color-text-muted Captions, placeholders, scrollbars
--color-border Rules and outlines of static elements
--color-input The surface of input, select and textarea
--color-button The surface of button
--color-brand Your accent: link underline, accent-color, primary button

The four status colors are the same in both schemes and have no pair behind them by default: --color-success, --color-warning, --color-danger and --color-info.

A whole theme

This is a realistic one, in full:

:root {
	--lightmode-color-brand: oklch(0.62 0.11 220);
	--darkmode-color-brand: oklch(0.72 0.13 220);
	--font-family: 'Inter', system-ui, sans-serif;
	--border-radius: 8px;
	--space-xs: 0.125rem;
}

--space-xs is the base of the spacing scale, each step is twice the one before, so moving it moves everything. Your theme does not even have to be loaded after Havelcss: an unlayered rule beats every layered one, whatever the order.

Download theme.example.css. It lists every token with its default, all of it commented out. Copy the lines you want to change and delete the rest.

Typed tokens

Wherever the syntax allows it, tokens are registered with @property and carry a type: a color, a length, a time. The browser checks what you assign against that type. A value that does not fit is discarded and the default stays in place, instead of the token going empty and every rule that reads it failing along with it.

Switching the scheme

Both schemes follow the operating system out of the box. To let the visitor decide, set data-color-scheme:

// 'dark', 'light', or remove the attribute to follow the system again
document.documentElement.dataset.colorScheme = 'dark';

It is not limited to <html>. The effective tokens stay unresolved until the element that finally reads them, so color-scheme keeps working further down the tree and a single panel can hold its own scheme:

A light island

With a link and a mark.

A dark island

With a link and a mark.