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: