Theme toggle
sitelo-ui resolves dark mode from prefers-color-scheme on its own — a site that is happy following the operating system needs nothing on this page. The toggle is for letting a reader override that.
It is one of the four components that need a script, because the choice lives in localStorage and only a script can read it. The button fetches that script itself, on the first press.
Setting it up
Two things in the head, and the button wherever it belongs:
import { styles, themeScript, themeToggle } from 'sitelo/ui'
head(
themeScript(), // applies the stored choice before the first paint
styles(),
)
body(
appBar({ brand: 'My site' },
appBarSpacer(),
appBarActions(themeToggle()),
),
)There is no third file. themeScript() is blocking and inline on purpose — anything deferred paints first, which is exactly the dark flash it exists to prevent — and the flip itself rides on the button:
<button data-su-theme-toggle
onclick="import('/su/theme.js').then(m=>m.toggle(this))">Pair the two. themeScript() is also what marks the toggle aria-pressed on load: nothing has been pressed yet, so the button itself cannot know which theme resolved.
The toggle
The icon is pure CSS, read straight off the theme attribute — so it is already correct on the first paint, before any script runs. It shows what a click will switch to.
stack({ direction: 'row', gap: 'md', align: 'center' },
themeToggle(),
themeToggle({ variant: 'soft' }),
themeToggle({ variant: 'outline' }),
)Those buttons work — this page loads the runtime. Clicking one sets data-su-theme on <html>, which is sitelo-ui’s own attribute, so only the sitelo-ui components on this page change. The rest of this site follows its own data-theme, set by the toggle in the top bar. On your site there would be only one of them.
In an app bar
appBar({ brand: 'sitelo' },
appBarNav(navLink({ href: '#docs', current: true }, 'Docs')),
appBarSpacer(),
appBarActions(
themeToggle(),
button({ size: 'sm' }, 'Get started'),
),
)How the theme resolves
In order: an explicit data-theme or data-su-theme on any ancestor wins; failing that, prefers-color-scheme decides. Both attribute names are honoured so sitelo-ui can sit inside a site that already has its own theme switch — which is exactly what these docs do.
Driving it yourself
The runtime exports the same functions the button uses, for a custom control, or a three-way light / dark / system picker.
import { getTheme, setTheme, toggleTheme } from 'sitelo/ui/client'
getTheme() // 'light' | 'dark' — resolved, not stored
toggleTheme() // flip
setTheme('dark') // pin
setTheme('system') // clear the override and follow the OS againProps
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Toggle dark mode' | Accessible name and tooltip. |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'ghost' | Button variant. |
color | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | Which palette to draw from. |
themeScript() takes an optional nonce, for a site with a content security policy.