Irozon Design System — design foundations, tokens and components by Irozon AB.
Irozon Design System · v1.0
Components / Theme Toggle

A centered dot, until it needs to say more.

.iz-theme-toggle is a compact Light/Dark control for navigation, app headers and product chrome — not a general binary setting. At rest it shows only a small state dot, no visible track. Hover or keyboard focus reveals a small vertical track and the dot moves to its current theme's position; leaving hover/focus collapses it back to center without changing the selected theme. Only a click or tap actually switches the theme.

Base class
.iz-theme-toggle on a real <button type="button">.
State
aria-pressed="true" = dark is currently selected (dot: lower position). "false"/absent = light (upper).
Hit target
44×44px — the visible graphic is much smaller; the button itself provides the full accessible target.
<button
  class="iz-theme-toggle"
  type="button"
  aria-label="Switch to dark theme"
  aria-pressed="false"
>
  <span class="iz-theme-toggle__track" aria-hidden="true">
    <span class="iz-theme-toggle__indicator"></span>
  </span>
</button>
Live specimen

Try it — hover, Tab to it, click it.

This one is wired up for real: it reads and writes this page's actual data-theme, the same state the masthead toggle above uses. Hover or focus it to see the reveal; click it to actually switch the whole page's theme.

Rest — centered dot, no visible track

Tab to it and watch the track reveal exactly as hover does; press Space or Enter to toggle. Move focus away (or the pointer off) without clicking and the track collapses back to center — the selected theme does not change until an actual click/activation.

State positions

Light is up. Dark is down. Always.

Consistent everywhere the control appears — the dot's position within the revealed track always means the same thing, regardless of the page's own current theme.

Light selected
Dot at the upper position within the track.
Dark selected
Dot at the lower position within the track.
Rest (either state)
Track invisible; dot centered in the full 44×44 hit area — not the track's center, the control's.
Colors

Ink on light. Lime on dark. Never a filled track.

The dot's color follows the page's actual ambient theme (not the toggle's own selected-state position) — Ink on a light surface, Lime on a dark one, the same guarded pattern design-tokens/themes.css uses everywhere else. The revealed track is always a quiet neutral surface with a restrained border — never filled Green or Lime.

Light page — Ink dot, neutral track
Dark page — Lime dot, dark neutral track

Lime was compared here against the light theme-text foreground at this same tiny 12px scale before settling on Lime: a small, precise dot is the same category of use as Tabs' selection marker or Switch's on-state track — brand color used functionally at a restrained scale, not a large field — so Lime reads as refined here rather than loud, consistent with that existing precedent.

Not a Switch

Two different controls, two different jobs.

A binary setting (Notifications, Auto-save, Dark mode as a settings-row item) — horizontal, always shows its track/state, no reveal interaction.
Theme Toggle
Compact Light/Dark chrome for a header or nav — quiet at rest, reveals only on interaction. Use Switch instead when Dark mode appears as an explicit row in a settings list.
Behavior

One tap is the whole action.

Touch
The centered dot is tappable via the full 44px button at all times — a tap toggles the theme immediately, it never takes a first tap to "reveal" and a second to act.
Keyboard
Tab reveals the track exactly like hover; Space/Enter toggles via native button activation — no custom key handling.
Motion
Track opacity/surface and the dot's position transition on --motion-duration-fast / --motion-ease-standard — the same restrained hover-state timing used everywhere else in the system. No spring, no bounce, no glow.
Reduced motion
All transitions drop under prefers-reduced-motion: reduce — the track appears/collapses and the dot repositions instantly; nothing about the state becomes unclear.
Focus
The ring sits on the compact revealed track, not the full 44×44 hit area — canonical Green (--color-focus), never Blue.
What this component does not own

Presentation only.

.iz-theme-toggle has no persistence and no FOUC-prevention logic — a consuming app owns reading the stored/OS-preferred theme before first paint, flipping data-theme, and persisting the choice. The live specimen above shows the minimal amount of controller code that requires (read current theme, toggle data-theme, update aria-pressed and the accessible label) — see this page's own inline script for the exact pattern.

React

ThemeToggle from @irozon/design-system-react.

Presentational only, by design — it takes isDark and onToggle and renders the control above; how (or whether) the choice persists across visits is your app's decision, same as the vanilla-JS pattern this page documents.

import { ThemeToggle } from "@irozon/design-system-react";

<ThemeToggle
  isDark={theme === "dark"}
  onToggle={() => setTheme(theme === "dark" ? "light" : "dark")}
/>