JSFiddle - React, Tailwind, and code Playground

by Jordan Sayner

HTML

<form class="theme-toggle theme-toggle--compact" role="radiogroup" aria-label="Color theme">
  <div class="segmented">
    <input type="radio" name="theme" id="t-light" value="light" />
    <label for="t-light" title="Light" aria-label="Light">
      <!-- Sun -->
      <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
        <path d="M12 4V2M12 22v-2M4 12H2M22 12h-2M5 5L3.6 3.6M20.4 20.4L19 19M5 19l-1.4 1.4M20.4 3.6L19 5" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/>
        <circle cx="12" cy="12" r="4" fill="currentColor"/>
      </svg>
      <span class="sr-only">Light</span>
    </label>

    <input type="radio" name="theme" id="t-dark" value="dark" />
    <label for="t-dark" title="Dark" aria-label="Dark">
      <!-- Moon -->
      <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
        <path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z" fill="currentColor"/>
      </svg>
      <span class="sr-only">Dark</span>
    </label>

    <input type="radio" name="theme" id="t-auto" value="auto" />
    <label for="t-auto" title="Use OS" aria-label="Auto (Use OS setting)">
      <!-- Desktop -->
      <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
        <rect x="3" y="4" width="18" height="12" rx="2" fill="currentColor"/>
        <path d="M8 20h8M10.5 16h3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
      </svg>
      <span class="sr-only">Auto</span>
    </label>
  </div>
</form>

CSS

/* --- Theme tokens (Auto = follow OS) --- */
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --fg: #111111;
  --accent: #283583;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1115;
    --fg: #e9edf1;
    --accent: #8aa2ff;
  }
}
html[data-theme="light"] { --bg:#fff; --fg:#111; --accent:#283583; }
html[data-theme="dark"]  { --bg:#0f1115; --fg:#e9edf1; --accent:#8aa2ff; }
body { background: var(--bg); color: var(--fg); }

/* --- Compact segmented control --- */
.theme-toggle--compact {
  --size: 36px;             /* control height */
  --radius: 999px;
  --pad: 4px;
  --border: 1px solid color-mix(in hsl, var(--fg) 22%, transparent);
  --track: color-mix(in hsl, var(--fg) 10%, transparent);
  --thumb: var(--bg);
  --icon: color-mix(in hsl, var(--fg) 70%, transparent);
  --icon-active: var(--accent);
  font: 500 13px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}

.theme-toggle--compact .segmented {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  inline-size: calc(var(--size) * 3 + var(--pad) * 2 + 8px); /* tight pill */
  max-inline-size: 100%;
  background: var(--track);
  border: var(--border);
  border-radius: var(--radius);
  overflow: clip;
}

.theme-toggle--compact input[type="radio"] {
  position: absolute; inset: 0; opacity: 0; pointer-events: none;
}

.theme-toggle--compact label {
  position: relative;
  block-size: var(--size);
  display: grid;
  place-items: center;
  cursor: pointer;
  z-index: 2;
  color: var(--icon);
  -webkit-tap-highlight-color: transparent;
}

.theme-toggle--compact .segmented::before {
  content: "";
  position: absolute;
  inset: var(--pad);
  inline-size: calc((100% - var(--pad)*2) / 3);
  block-size: calc(100% - var(--pad)*2);
  background: var(--thumb);
  border-radius: calc(var(--radius) - var(--pad));
  box-shadow: 0 1px 2px rgba(0,0,0,.12);
  transform: translateX(0%);
  transition:...

JavaScript

(() => {
  const STORAGE_KEY = 'theme-preference'; // 'light' | 'dark' | 'auto'
  const radios = document.querySelectorAll('.theme-toggle input[type="radio"]');
  const root = document.documentElement;

  function applyTheme(mode) {
    if (mode === 'light' || mode === 'dark') {
      root.setAttribute('data-theme', mode);
    } else {
      // auto: follow OS — remove attribute so @media rules win
      root.removeAttribute('data-theme');
    }
  }

  // init: load saved choice or default to auto
  const saved = localStorage.getItem(STORAGE_KEY) || 'auto';
  applyTheme(saved);
  const toCheck = document.querySelector(`.theme-toggle input[value="${saved}"]`);
  if (toCheck) toCheck.checked = true; else document.getElementById('theme-auto').checked = true;

  // change handler
  radios.forEach(r => r.addEventListener('change', (e) => {
    const mode = e.target.value; // light | dark | auto
    applyTheme(mode);
    localStorage.setItem(STORAGE_KEY, mode);
  }));
})();