Theme toggle
Usage
Let people choose how Fleetline looks. Dispatchers on night shift prefer dark; mechanics outdoors usually need light. System is the default and follows the device setting.
- Cycling button in the app header: one click per step, the name always states the current theme ("Theme: dark").
- Radio group on the settings page: all options visible, nothing hidden behind clicks.
- Labels are nouns: "System", "Light", "Dark". Don't describe the effect ("Easy on the eyes").
- High contrast follows the OS (
prefers-contrast: more); don't add it as a fourth option unless research asks for it.
Anatomy
- Button —
.btn.btn--ghost.theme-toggle[data-theme-cycle] - Icons — three
svg.theme-toggle__icon[data-for]; only the current one shows - Label —
.theme-toggle__label[data-theme-label], updated byjs/theme.js - Radio group —
fieldset.theme-choice+legend+ threelabel.theme-choice__optionwrapping a radio
Examples
The first two demos are live and change this page's theme. The state demos are frozen copies (data-preview on the button, inert radio groups).
Theme toggle
Live: cycles System → Light → Dark; the name says the current theme.
Live: settings form. System follows the operating system.
<!-- In <head>, before stylesheets: no-flash snippet -->
<script>try{var t=localStorage.getItem("ds-theme");if(t&&t!=="system")document.documentElement.dataset.theme=t}catch(e){}</script>
<!-- Cycling button (header) -->
<button type="button" class="btn btn--ghost theme-toggle" data-theme-cycle data-themes="system light dark">
<svg class="theme-toggle__icon" data-for="system" aria-hidden="true" focusable="false"><use href="#icon-monitor"></use></svg>
<svg class="theme-toggle__icon" data-for="light" aria-hidden="true" focusable="false"><use href="#icon-sun"></use></svg>
<svg class="theme-toggle__icon" data-for="dark" aria-hidden="true" focusable="false"><use href="#icon-moon"></use></svg>
<span class="theme-toggle__label" data-theme-label>Theme: system</span>
</button>
<!-- Radio group (settings) -->
<fieldset class="theme-choice" data-theme-choice>
<legend class="theme-choice__legend">Appearance</legend>
<label class="theme-choice__option"><input type="radio" name="theme" value="system" checked>…System</label>
<label class="theme-choice__option"><input type="radio" name="theme" value="light">…Light</label>
<label class="theme-choice__option"><input type="radio" name="theme" value="dark">…Dark</label>
</fieldset>
<script type="module">
import { initTheme } from "./js/theme.js";
import { init } from "./js/theme-toggle.js";
initTheme(); init();
</script>
API
| Hook | Values | Purpose |
|---|---|---|
.theme-toggle | block class on a .btn | Cycling theme button |
data-theme-cycle, data-themes | attribute | Enhanced by js/theme.js; themes in cycle order |
.theme-toggle__icon[data-for] | part | System / light / dark icon |
[data-theme-label] | attribute | Text updated to "Theme: …" |
.theme-choice[data-theme-choice] | block + attribute | Radio group, wired by js/theme-toggle.js |
.theme-choice__option | part | Label wrapping a radio |
data-preview | docs-only attribute | Freeze the icon of a theme |
setTheme(), getTheme(), onThemeChange() | JS (theme.js) | Runtime API |
Do and don't
<button aria-label="Dark mode" aria-pressed="false"> on a three-way cycle
aria-pressed on a button that cycles through three themes — pressed/unpressed can't describe it.Accessibility
| Key | Behavior |
|---|---|
| Enter / Space (button) | Switches to the next theme; the name updates ("Theme: light") |
| Tab (radio group) | Moves into the group, to the checked option |
| ↑ ↓ ← → | Select the previous / next theme (native radios) |
- The choice persists in
localStorage(try/catch for private mode) and syncs across tabs through thestorageevent. - The no-flash snippet in
<head>applies the saved theme before first paint. - Checked option: selected fill, border and semibold label — not color alone. Forced colors use
Highlight.
Tokens
| Custom property | Purpose |
|---|---|
--color-action-secondary-* | Option rest / hover |
--color-selected-bg | -fg | -border | Checked option |
--size-control-md, --button-radius | Option size and shape |
--icon-size-sm, --icon-stroke | Icons |
--focus-ring-* | Focus ring |