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);
}));
})();