Source Code

<div class="tt-wrap">
  <div class="tt-segmented" id="ttSegmented">
    <div class="tt-highlight" id="ttHighlight"></div>
    <button class="tt-option tt-active" data-mode="light">
      <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="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>
      Light
    </button>
    <button class="tt-option" data-mode="dark">
      <svg width="15" height="15" 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 1111.2 3a7 7 0 009.8 9.8z"/></svg>
      Dark
    </button>
    <button class="tt-option" data-mode="system">
      <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>
      System
    </button>
  </div>

  <div class="tt-preview" id="ttPreview" data-theme="light">
    <div class="tt-preview-header">
      <span class="tt-dot" style="background:#f87171"></span>
      <span class="tt-dot" style="background:#fbbf24"></span>
      <span class="tt-dot" style="background:#34d399"></span>
    </div>
    <h3>Preview panel</h3>
    <p>This card restyles instantly based on the selected theme mode, including when "System" resolves from your OS setting.</p>
    <button class="tt-preview-btn">Sample button</button>
  </div>
</div>

Three-Way Theme Toggle Switch — Free HTML CSS JS Snippet

Three-Way Theme Toggle Switch · Buttons · Plain HTML, CSS & JS · Live preview

What's included

Features

Three-way segmented control (Light/Dark/System) with a sliding highlight behind the active option
"System" option resolves live from window.matchMedia("(prefers-color-scheme: dark)")
Live re-resolution of the System theme if the OS preference changes while System stays selected
Preview panel restyled entirely through CSS custom properties scoped to a data-theme attribute
Smooth CSS transform transition for the highlight slide, no JS animation loop
Icon-labeled options (sun/moon/monitor) for quick visual scanning
Preview panel demonstrates borders, muted text, and an accent button all reacting to the same toggle
Vanilla JS state kept in the DOM (active class + data-theme attribute), no external state library

About this UI Snippet

Three-Way Theme Toggle Switch — Light / Dark / System Segmented Control

Screenshot of the Three-Way Theme Toggle Switch snippet rendered live

Most theme toggles are a simple two-state switch, but a "System" option — deferring to the operating system's preference — is what most production apps actually ship. This snippet implements a three-way segmented control (Light / Dark / System) with a sliding highlight behind the active option, wired to a live preview panel that restyles through CSS custom properties.

The sliding highlight

.tt-highlight is a single absolutely-positioned element sized to one-third of the segmented control's width. Selecting an option translates it via transform: translateX(index * 100%), animated with a CSS transition — a cheap, GPU-accelerated slide rather than re-rendering separate active backgrounds for each button.

Resolving "System" at selection time

Choosing "System" doesn't set a fixed theme; resolveSystemTheme() checks window.matchMedia('(prefers-color-scheme: dark)').matches at the moment of selection to decide whether the preview should currently look light or dark. A change listener on that same media query re-resolves the theme live if the OS preference changes while "System" is still selected — the preview updates without requiring the user to click anything again.

CSS custom properties drive the actual restyle

The preview panel defines its light-mode colors as CSS custom properties (--tt-surface, --tt-text, --tt-border, --tt-muted) directly on .tt-preview, then overrides all four inside a [data-theme="dark"] attribute selector scoped to the same element. Every color used inside the panel — background, text, borders, muted copy — reads from these variables, so setting one attribute (data-theme) is enough to restyle the entire panel consistently.

Step by step

How to Use

  1. 1
    Load the snippetClick "Three-Way Theme Toggle Switch" in the sidebar to load its HTML, CSS, and JS into the editor panels. The preview updates instantly.
  2. 2
    Edit the codeModify any panel — HTML, CSS, or JS. The preview refreshes as you type. Use Reset in each panel header to restore the original.
  3. 3
    Preview on devicesClick the Mobile (375px), Tablet (768px), or Desktop buttons in the preview header to check responsiveness.
  4. 4
    Export in your formatClick "HTML" to download a standalone file, "JSX" for a React component, "Tailwind" for a React + Tailwind CSS component, "Tailwind HTML" for a standalone HTML file with Tailwind CDN, "Vue" for a Vue 3 SFC with <template>/<script setup>/<style scoped>, or "Angular" for a standalone Angular .component.ts file. "Copy all" copies the full code to clipboard.
  5. 5
    Save your versionClick "Save as", type a name, and press Enter. Your snippet saves to IndexedDB and appears in the Saved tab.

Real-world uses

Common Use Cases

Application settings panels
The standard three-option theme control used in most modern app preference screens.
Design system theme documentation
Demonstrate how components restyle across light, dark, and OS-driven themes in one control.
Reference for prefers-color-scheme handling
A concrete example of resolving and live-updating from the OS color scheme preference.
Teaching CSS custom property theming
Shows how scoping variable overrides to an attribute selector drives a full component restyle.

Got questions?

Frequently Asked Questions

It reads window.matchMedia("(prefers-color-scheme: dark)").matches at the moment System is selected. If the OS preference changes afterward while System is still active, a change listener on that same media query re-resolves and reapplies the theme automatically.

A single absolutely-positioned element sized to one-third the control's width is translated horizontally by a multiple of 100% of its own width based on the selected option's index, animated with a CSS transition on transform.

All of its colors are CSS custom properties defined on .tt-preview. A [data-theme="dark"] attribute selector scoped to the same element redefines those properties; toggling the attribute is the only change needed to restyle everything inside it.

Yes — add another .tt-option button and adjust the highlight width percentage and translateX multiplier to match the new number of options, plus a corresponding CSS custom property override block.