Overview
PimentCSS switches palettes through data-theme on <html>. Semantic tokens in tokens/semantic.css update surfaces, text, and borders for light and dark. The .theme-toggle component styles the control; a small script (yours or the docs reference) keeps dataset.theme, storage, and UI in sync.
| Piece | Source | Role |
|---|---|---|
| Semantic tokens | tokens/semantic.css | --surface-*, --text-* per theme |
| Component SCSS | scss/components/_theme-toggle.scss | .theme-toggle, modifiers |
| Icons (sun / moon) | assets/icons/theme-toggle/ | Dedicated SVG in doc assets |
| Dark overrides | [data-theme="dark"] | Explicit dark palette |
| System fallback | prefers-color-scheme: dark | When data-theme is unset |
| Docs reference JS | docs-site/src/lib/theme.ts | Bootstrap, storage, applyTheme |
Prerequisites
- PimentCSS installed, Installation (theme toggle ships in the default bundle).
- Semantic colors, understand
data-themeand token pairs from Colors. - Optional script, required only to persist choice and sync multiple controls on the page.
How it works
Set data-theme="light" or data-theme="dark" on the document root. PimentCSS maps each value to semantic variables; omitting the attribute follows the OS via prefers-color-scheme (unless you force data-theme="light").
…
When using localStorage, inject a blocking bootstrap script in <head> before CSS paints (see Bootstrap before paint below).
Compact control (header / toolbar)
Icon-only segmented control for navigation bars. Uses role="group", aria-pressed on each option, and data-theme-toggle for scripting. Active option gets .is-active and --shadow-xs.
<div class="theme-toggle theme-toggle--compact" role="group" aria-label="Color mode" data-theme-toggle=""><button type="button" class="theme-toggle__option is-active" data-theme-value="light" aria-pressed="true" aria-label="Light mode">
<svg class="theme-toggle__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M12 3v2M12 19v2M5.64 5.64l1.42 1.42M16.94 16.94l1.42 1.42M3 12h2M19 12h2M5.64 18.36l1.42-1.42M16.94 7.06l1.42-1.42"></path>
</svg>
</button><button type="button" class="theme-toggle__option" data-theme-value="dark" aria-pressed="false" aria-label="Dark mode">
<svg class="theme-toggle__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M13.9 4.5a7.5 7.5 0 1 0 6.6 11.9A8.5 8.5 0 0 1 13.9 4.5Z"></path>
</svg>
</button></div>
Switch variant (settings / forms)
Checkbox styled as a switch for preference panels. Checked means dark mode. Visible label text stays beside the track; the input is visually hidden but focusable.
Preferences
<label class="theme-toggle theme-toggle--switch">
<input type="checkbox" class="theme-toggle__input" role="switch" aria-checked="false" data-theme-switch="">
<span class="theme-toggle__track"><span class="theme-toggle__knob"></span></span>
<span class="theme-toggle__label">Dark mode</span>
</label>
Wire with JavaScript
PimentCSS does not ship runtime theme logic. Copy the pattern from docs-site/src/lib/theme.ts (used on this site) or adapt the steps below.
-
Bootstrap before paint
ReadlocalStoragekeypimentcss-theme, elseprefers-color-scheme, and setdocument.documentElement.dataset.themein<head>.<script>(function () { try { var k = 'pimentcss-theme'; var s = localStorage.getItem(k); var t = s === 'light' || s === 'dark' ? s : window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; document.documentElement.dataset.theme = t; } catch (e) { document.documentElement.dataset.theme = 'light'; } })();</script> -
Apply and persist
On change, setdata-themeand optionallylocalStorage.setItem("pimentcss-theme", theme). Clear storage to follow the OS again.document.documentElement.dataset.theme = 'dark'; localStorage.setItem('pimentcss-theme', 'dark'); -
Sync all controls
Update every[data-theme-toggle]button and[data-theme-switch]input when the theme changes (including the docs header).import { applyTheme, resolveTheme, setTheme, type ThemeMode } from './theme'; function syncThemeUi(theme: ThemeMode) { document.querySelectorAll('[data-theme-toggle]').forEach((root) => { root.querySelectorAll<HTMLButtonElement>('[data-theme-value]').forEach((btn) => { const active = btn.dataset.themeValue === theme; btn.classList.toggle('is-active', active); btn.setAttribute('aria-pressed', active ? 'true' : 'false'); }); }); document.querySelectorAll<HTMLInputElement>('[data-theme-switch]').forEach((input) => { input.checked = theme === 'dark'; input.setAttribute('aria-checked', input.checked ? 'true' : 'false'); }); } applyTheme(resolveTheme()); syncThemeUi(resolveTheme()); document.querySelectorAll('[data-theme-toggle]').forEach((root) => { root.querySelectorAll<HTMLButtonElement>('[data-theme-value]').forEach((btn) => { btn.addEventListener('click', () => { const next = btn.dataset.themeValue as ThemeMode; if (next !== 'light' && next !== 'dark') return; setTheme(next); syncThemeUi(next); }); }); }); document.querySelectorAll<HTMLInputElement>('[data-theme-switch]').forEach((input) => { input.addEventListener('change', () => { const next: ThemeMode = input.checked ? 'dark' : 'light'; setTheme(next); syncThemeUi(next); }); });
Demos on this page share the same sync as the header toggle. Try switching here and in the toolbar above.
Class reference
Optional namespace via Sass $prefix (empty by default).
| Class / attribute | Description |
|---|---|
.theme-toggle | Segmented container (muted surface, border, round) |
.theme-toggle--compact | Icon-only options, --min-touch-target (2.75rem) |
.theme-toggle--switch | Switch layout with visible label |
.theme-toggle__option | Segment button; :focus-visible ring |
.theme-toggle__option.is-active | Selected mode (also [aria-pressed="true"]) |
.theme-toggle__input | Visually hidden checkbox (role="switch") |
.theme-toggle__track / __knob | Switch visuals; knob moves when checked |
data-theme-toggle | Root for segmented wiring |
data-theme-value | light or dark on segment buttons |
data-theme-switch | Checkbox switch (checked = dark) |
Customize (Sass)
Dark semantic tokens compile by default ($enable-dark-theme: true in scss/abstracts/_variables.scss). Override surfaces globally, not only the toggle:
@use "pimentcss-design-system" with (
$enable-dark-theme: true,
);
// Optional: tune compact chrome shadow via depth tokens
// $shadow-xs: 0 1px 3px oklch(40% 0.03 262 / 0.12);
See Customization and Depth & shadows for elevation tokens used on the active segment.
Accessibility (RGAA / WCAG)
Color mode without barriers
- Name and state, segmented:
aria-labelon the group plusaria-pressedper button. Switch:role="switch"andaria-checkedkept in sync withchecked. - Do not rely on iconography alone (RGAA 10.7). Compact variant uses
aria-label="Light mode"/Dark mode; switch variant exposes visible textDark mode. - Focus visible, 3px
--border-focusoutline on segment buttons; keyboard users can tab to the switch input. - Touch target, segment options use
--min-touch-target(2.75rem), including.theme-toggle--compact. - Contrast, re-check active segment text (
--text-actionon--surface-primary) in both themes after brand overrides. - Reduced motion, transitions respect
prefers-reduced-motionwhen set in global CSS.