The Tri-State Toggle Trap

Open almost any modern site's header and you'll find the same widget: a three-way theme switcher offering Light, Dark, and System. It looks thorough. It feels respectful of user choice. And in most cases, it's quietly working against the visitor's actual goal.

Think about why someone lands on your page. They want to read an article, buy something, or solve a problem. The theme toggle is tangential to that intent. Every extra option in a persistent UI element is one more micro-decision the visitor didn't ask for. Cognitive dissonance compounds: "Wait, what's the difference between Dark and System? Is System following my OS? Which one is active right now?"

The core argument, articulated well by Lea Verou's discussion on CSS-Tricks, is simple: a two-state control can express all three states β€” as long as you apply the override the first time it becomes relevant.

Web developer inspecting light and dark mode toggle UI in browser devtools Programming Illustration

The Two-State Pattern in Code

The idea: default to the system preference via prefers-color-scheme, and only store an override in localStorage when the user explicitly flips the toggle. No "System" button needed.

// theme.js β€” two-state toggle with system fallback
const root = document.documentElement;
const stored = localStorage.getItem('theme'); // 'light' | 'dark' | null

// 1. Resolve initial theme: stored override > system preference
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = stored ?? (systemPrefersDark ? 'dark' : 'light');
root.dataset.theme = initialTheme;

// 2. Show only the *opposite* option in the UI
function renderToggle() {
  const current = root.dataset.theme;
  // If we're in light, offer "Dark"; if dark, offer "Light".
  document.getElementById('theme-toggle').textContent =
    current === 'dark' ? 'β˜€οΈ Light' : 'πŸŒ™ Dark';
}

// 3. On click: flip, persist, re-render
document.getElementById('theme-toggle').addEventListener('click', () => {
  const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
  root.dataset.theme = next;
  localStorage.setItem('theme', next); // override stored
  renderToggle();
});

// 4. React to OS changes ONLY when no override exists
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (localStorage.getItem('theme')) return; // user has overridden
  root.dataset.theme = e.matches ? 'dark' : 'light';
  renderToggle();
});

renderToggle();
/* style.css β€” CSS variables driven by the data-theme attribute */
:root[data-theme="light"] {
  --bg: #ffffff;
  --fg: #111111;
}
:root[data-theme="dark"] {
  --bg: #111111;
  --fg: #f5f5f5;
}

body {
  background: var(--bg);
  color: var(--fg);
  transition: background 0.2s ease, color 0.2s ease;
}

The magic is in step 4: system changes propagate only until the user makes a choice. After that, localStorage is the source of truth. And crucially β€” the visitor never sees a "System" button, because "System" isn't a state they need to manage. It's the default they fall back to.

UI designer sketching two-state theme toggle wireframe on tablet Coding Session Visual

When Three States Actually Earn Their Keep

Before you rip out every tri-state toggle in your codebase, note the exceptions. The two-state pattern is guidance for persistent, always-visible toggles β€” the ones in your site header that compete with the visitor's primary intent. Three states do make sense in two narrow scenarios:

1. The toggle lives in a dedicated settings panel

If the theme control is buried in a Preferences page β€” somewhere the user navigated intentionally β€” the cognitive cost of a third option drops to near zero. In that context, explicit Light/Dark/System radios are actually clearer than a clever two-state flip.

2. You offer more than two color schemes

If your site ships Solarized, Nord, Dracula, and a high-contrast accessibility mode alongside the system default, "System" becomes a meaningful fifth option rather than a redundant third. The two-state pattern assumes exactly two schemes.

Watch out: the toggle-back edge case

What happens if a user starts on System (Light), flips to Dark, then flips back? Do they land on System again, or on a Light override? The honest answer: it doesn't matter. As Lea points out, the divergence only becomes observable when the OS later switches to dark β€” and at that point, fixing it is a single click away. Don't over-engineer a state machine for a control your visitor barely notices.

The real alternative: no toggle at all

If your site's design holds up in both schemes, consider shipping zero toggles and trusting prefers-color-scheme entirely. The browser already exposes this in the user's OS settings. A toggle is a feature you have to maintain, test across browsers, and explain. Sometimes the best UX is the one you don't build.

Laptop screen showing CSS prefers-color-scheme media query code editor

The Takeaway

Less UI that accomplishes the same as more UI is almost always the right call β€” especially for controls that sit between the visitor and their goal. The two-state toggle pattern gives you:

  • Full user control (override persisted in localStorage)
  • System preference respected (via prefers-color-scheme)
  • One less button cluttering your navigation

If you're building a component library or design system, bake this in as the default. Reserve tri-state toggles for dedicated settings pages and multi-scheme themes. And if you're curious how modern on-device AI tooling is being wired into mobile apps, the ADK for Kotlin & Android tutorial is a solid next read β€” the same "less surface area, more clarity" principle applies to agent UIs. For teams deploying inference infrastructure behind these experiences, the SageMaker HyperPod inference operator guide covers the backend side.

Next steps to explore: audit your own site's header for redundant controls, run a quick accessibility pass on your prefers-color-scheme handling (does it respect prefers-contrast?), and check whether your toggle is announced correctly to screen readers via aria-pressed.

This content was drafted using AI tools based on reliable sources, and has been reviewed by our editorial team before publication. It is not intended to replace professional advice.