webdevpuneet.com

Color Mode Toggle — Light/Dark/System Switch UI

Color Mode Toggle · Buttons · Plain HTML, CSS & JS · Live preview

CategoryButtons

Copy & paste

Source Code

<div class="cmt-demo" id="cmtDemo">
  <div class="cmt-bar">
    <span class="cmt-logo">◆ Acme</span>
    <div class="cmt-switch" role="radiogroup" aria-label="Color theme">
      <button type="button" class="cmt-opt" data-mode="light" aria-label="Light">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
      </button>
      <button type="button" class="cmt-opt" data-mode="system" aria-label="System">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
      </button>
      <button type="button" class="cmt-opt" data-mode="dark" aria-label="Dark">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
      </button>
      <span class="cmt-thumb" id="cmtThumb"></span>
    </div>
  </div>

  <div class="cmt-content">
    <h3>Good morning</h3>
    <p>This panel previews your theme. Pick Light, Dark, or System — System follows your device setting automatically.</p>
    <div class="cmt-chips"><span>Live preview</span><span class="cmt-state" id="cmtState">System · dark</span></div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Three-way light/dark/system
A segmented switch with the System option most users actually want as a default.
Live OS-theme following
In System mode, a matchMedia change listener re-applies the theme when the device setting flips — no reload.
CSS-variable theming
Tokens on the container with a [data-theme="dark"] override mean switching is one attribute change, not per-element classes.
Smooth color crossfade
Transitions on the theme variables make switching a gentle fade rather than an abrupt flip.
Sliding thumb indicator
A transform-animated thumb slides under the active option with spring easing, GPU-cheap and smooth.
Resolved-state readout
A status chip shows the chosen mode and, for System, what it currently maps to ("System · dark").
Persistence hooks marked
Comments mark exactly where to add localStorage save and the head inline script to prevent a theme flash.
Accessible icon switch
role="radiogroup" with per-button aria-labels, built from real buttons so keyboard and screen-reader work natively.

About this UI Snippet

Color Mode Toggle — Three-Way Light / Dark / System Theme Switch

Screenshot of the Color Mode Toggle snippet rendered live

A two-state dark-mode toggle forces a choice the user shouldn't have to make: it ignores that their device already has a light/dark preference that changes with time of day. The modern pattern is a three-way switch — Light, Dark, and System — where System defers to the OS setting and updates live when it changes. This snippet builds that segmented theme control in plain HTML, CSS, and vanilla JavaScript, with a sliding thumb, CSS-variable theming, and correct System behavior.

Why "System" is the important option

"System" (sometimes "Auto") is what most users actually want as a default: it follows the device's prefers-color-scheme, so the site is light during the day and dark at night without anyone touching a setting. The key behavior is that System isn't a snapshot — it's a *live* binding. This snippet listens to the matchMedia('(prefers-color-scheme: dark)') change event and re-applies the theme whenever the OS flips, but only while the user is in System mode. Pick Light or Dark explicitly and you override the OS; pick System and you hand control back to it. Getting this live-follow behavior right is what separates a real three-way toggle from one that just has a third button.

Theming with CSS custom properties

The whole component themes through CSS variables. :root-equivalent tokens on the container (--bg, --text, --border, --accent) define the light theme, and a [data-theme="dark"] selector overrides them for dark. Every element references the variables, so switching themes is a single attribute change on one element — no per-element class toggling. A transition on the color properties makes the switch a smooth crossfade rather than an abrupt flip. This is the standard, scalable way to theme a site: define tokens once, flip one attribute.

The sliding thumb

The three options sit in a pill, and a thumb slides beneath the active one using transform: translateX() (the index times the option width), with a slight spring easing. Animating transform keeps the motion GPU-cheap and smooth, and the active icon tints to the accent color so the selection is clear from both the thumb position and the color. A small status chip echoes the current mode and, in System mode, the resolved theme ("System · dark") so the user can see what System currently maps to.

Scoped demo vs. real app

This demo applies the theme to its own container (.cmt-demo) so the surrounding page stays neutral — but in a real app you'd set data-theme on <html> and persist the choice to localStorage, then read it back on load (ideally in a tiny inline script in the <head>, before first paint, to avoid a flash of the wrong theme). The code comments mark exactly where those two lines go. The resolution logic — mode → resolved theme — is identical whether you scope it to a container or the whole document.

Accessible and keyboard-ready

The switch is a role="radiogroup" of icon buttons, each with an aria-label (Light / System / Dark), so it's operable and announced correctly. Building it from real buttons means keyboard focus and activation work without custom handling, and the visual selection never relies on color alone — the thumb position is a second, redundant indicator.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA themed preview panel renders with a Light / System / Dark switch; the thumb starts on System.
  2. 2
    Pick a modeClick Light or Dark to set the theme explicitly; the panel crossfades and the thumb slides to your choice.
  3. 3
    Use SystemChoose System to follow your device's setting — the status chip shows what it currently resolves to ("System · dark").
  4. 4
    Change your OS themeWhile in System mode, flip your operating system's appearance — the panel updates live without a click.
  5. 5
    Apply to your whole siteSet data-theme on <html> instead of the demo container and define your token variables on :root and [data-theme="dark"].
  6. 6
    Persist the choiceSave the mode to localStorage on change and read it back in a head inline script before first paint to avoid a theme flash.

Real-world uses

Common Use Cases

Site-wide theme control
The header theme switch for any site supporting dark mode — pair with a color theme switcher for accent-color choices too.
App and dashboard settings
Let users set appearance in account settings with the System default for automatic day/night.
Documentation and reading sites
Give readers a comfortable theme that respects their device preference — pair with a faq search accordion for the help section.
Design systems and component libraries
Demonstrate light/dark token theming as a reusable control across products.
Portfolio and marketing sites
Offer a polished theme switch that follows the visitor's OS by default.
Learning prefers-color-scheme handling
A reference for live System-theme following and CSS-variable theming — compare with a dark mode toggle for the two-state version.
Related: FAB Speed Dial Menu
See the FAB Speed Dial Menu for a related buttons pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Define your token variables on :root (light) and a html[data-theme="dark"] selector (dark overrides), set data-theme on document.documentElement instead of the demo container, and have every component reference the variables. Switching then re-themes the entire page from one attribute change.

Read the saved mode from localStorage and set data-theme on <html> in a tiny synchronous inline <script> in the <head>, before any CSS or content paints — this applies the correct theme before first render. Doing it in a deferred script or after hydration causes a visible flash (FOUC) of the default theme first.

"Dark" is an explicit override that stays dark regardless of the OS. "System" defers to the device's prefers-color-scheme and changes live when the OS does — so a System user on a device that switches to dark at sunset gets dark automatically, while a Dark user is always dark. Persisting the mode (light/dark/system), not just the resolved theme, preserves this distinction.

On every change, localStorage.setItem('theme', mode) storing the mode value ('light' | 'dark' | 'system'). On load, read it back, default to 'system' if absent, and apply. Store the mode rather than the resolved theme so a System user keeps following the OS rather than being pinned to whatever it resolved to last time.

In React, hold the mode in useState (initialized from localStorage), apply data-theme in a useEffect, and subscribe to the matchMedia change event for System; in Vue, use ref() with onMounted/watch; in Angular, use a service with a BehaviorSubject and Renderer2. The resolution logic (mode → theme) and the matchMedia listener port unchanged.