Skip to main content
PimentCSS v1.0.1, what's new
Home

Theme toggle

Light / dark mode control for headers, menus, and settings panels.

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.

PieceSourceRole
Semantic tokenstokens/semantic.css--surface-*, --text-* per theme
Component SCSSscss/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 fallbackprefers-color-scheme: darkWhen data-theme is unset
Docs reference JSdocs-site/src/lib/theme.tsBootstrap, storage, applyTheme

Prerequisites

  • PimentCSS installed, Installation (theme toggle ships in the default bundle).
  • Semantic colors, understand data-theme and 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.

Application

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

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.

  1. Bootstrap before paint

    Read localStorage key pimentcss-theme, else prefers-color-scheme, and set document.documentElement.dataset.theme in <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>
  2. Apply and persist

    On change, set data-theme and optionally localStorage.setItem("pimentcss-theme", theme). Clear storage to follow the OS again.
    document.documentElement.dataset.theme = 'dark';
    localStorage.setItem('pimentcss-theme', 'dark');
  3. 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 / attributeDescription
.theme-toggleSegmented container (muted surface, border, round)
.theme-toggle--compactIcon-only options, --min-touch-target (2.75rem)
.theme-toggle--switchSwitch layout with visible label
.theme-toggle__optionSegment button; :focus-visible ring
.theme-toggle__option.is-activeSelected mode (also [aria-pressed="true"])
.theme-toggle__inputVisually hidden checkbox (role="switch")
.theme-toggle__track / __knobSwitch visuals; knob moves when checked
data-theme-toggleRoot for segmented wiring
data-theme-valuelight or dark on segment buttons
data-theme-switchCheckbox 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:

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-label on the group plus aria-pressed per button. Switch: role="switch" and aria-checked kept in sync with checked.
  • Do not rely on iconography alone (RGAA 10.7). Compact variant uses aria-label="Light mode" / Dark mode; switch variant exposes visible text Dark mode.
  • Focus visible, 3px --border-focus outline 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-action on --surface-primary) in both themes after brand overrides.
  • Reduced motion, transitions respect prefers-reduced-motion when set in global CSS.

Next steps