Skip to content

Three-State Toggle

A copy-paste theme toggle component in pure HTML, CSS & vanilla JS. Zero dependencies, framework-agnostic, MIT-licensed.

Theme ToggleHTMLCSSJavaScriptany framework

Copy into your project

HTML
<!-- Three-State Toggle — Light / System / Dark -->
<div class="nuda-three" role="radiogroup" aria-label="Theme">
  <button class="nuda-three__opt"           type="button" role="radio" aria-checked="false" data-theme="light">…</button>
  <button class="nuda-three__opt is-active" type="button" role="radio" aria-checked="true"  data-theme="system">…</button>
  <button class="nuda-three__opt"           type="button" role="radio" aria-checked="false" data-theme="dark">…</button>
  <span class="nuda-three__pill"></span>
</div>
CSS
/* Three-State Toggle
   Segmented Light / System / Dark with sliding pill.
   Customize: --three-accent, --three-size */

.nuda-three {
  --three-accent: #e4ff54;
  --three-size: 32px;
  position: relative;
  display: inline-flex;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 999px;
  padding: 4px;
}

.nuda-three__opt {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--three-size);
  height: var(--three-size);
  background: transparent;
  border: none;
  color: #a1a1aa;
  cursor: pointer;
  border-radius: 999px;
  transition: color 0.3s;
}

.nuda-three__opt svg { width: 16px; height: 16px; }

.nuda-three__opt.is-active { color: var(--three-accent); }

.nuda-three__pill {
  position: absolute;
  z-index: 1;
  top: 4px;
  left: 4px;
  width: var(--three-size);
  height: var(--three-size);
  background: rgba(228, 255, 84, 0.12);
  border: 1px solid rgba(228, 255, 84, 0.25);
  border-radius: 999px;
  transform: translateX(var(--three-size));
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 0 8px rgba(228, 255, 84, 0.2);
}

/* Move pill via inline transform: translateX(0) | calc(var(--three-size)) | calc(var(--three-size) * 2) */
JavaScript
/* Three-State Toggle — Light / System / Dark with system-pref watch. */

(function () {
  var group = document.querySelector('.nuda-three');
  if (!group) return;
  var opts = group.querySelectorAll('.nuda-three__opt');
  var pill = group.querySelector('.nuda-three__pill');
  var KEY  = 'theme';
  var size = parseFloat(getComputedStyle(group).getPropertyValue('--three-size')) || 32;

  function apply(theme) {
    opts.forEach(function (o, i) {
      var active = o.dataset.theme === theme;
      o.classList.toggle('is-active', active);
      o.setAttribute('aria-checked', String(active));
      if (active) pill.style.transform = 'translateX(' + (i * size) + 'px)';
    });

    var dark = theme === 'dark' ||
      (theme === 'system' &&
       window.matchMedia('(prefers-color-scheme: dark)').matches);
    document.documentElement.classList.toggle('dark', dark);
  }

  opts.forEach(function (o) {
    o.addEventListener('click', function () {
      var theme = o.dataset.theme;
      localStorage.setItem(KEY, theme);
      apply(theme);
    });
  });

  apply(localStorage.getItem(KEY) || 'system');
})();

How to use Three-State Toggle

Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.

Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.

More theme toggle components

← Browse all NudaUI components