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.
Four more tokens set the edges and the shape.
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.