Skip to content

Contrast Stepper

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

Theme ToggleHTMLJavaScriptCSSany framework

Copy into your project

HTML
<!-- Contrast Stepper — Normal / High / Max, radiogroup semantics -->
<div class="nuda-tt2-contrast" role="radiogroup" aria-label="Contrast level">
  <button class="nuda-tt2-contrast__opt" type="button" role="radio" aria-checked="false" data-level="normal">
    <span class="nuda-tt2-contrast__bars" aria-hidden="true">
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
    </span>
    <span class="nuda-tt2-contrast__label">Normal</span>
  </button>
  <button class="nuda-tt2-contrast__opt is-active" type="button" role="radio" aria-checked="true" data-level="high">
    <span class="nuda-tt2-contrast__bars" aria-hidden="true">
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
    </span>
    <span class="nuda-tt2-contrast__label">High</span>
  </button>
  <button class="nuda-tt2-contrast__opt" type="button" role="radio" aria-checked="false" data-level="max">
    <span class="nuda-tt2-contrast__bars" aria-hidden="true">
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
      <span class="nuda-tt2-contrast__bar"></span>
    </span>
    <span class="nuda-tt2-contrast__label">Max</span>
  </button>
</div>
JavaScript
/* Contrast Stepper — radiogroup: one option active at a time. */
(function () {
  var group = document.querySelector('.nuda-tt2-contrast');
  if (!group) return;
  var opts = group.querySelectorAll('.nuda-tt2-contrast__opt');
  opts.forEach(function (opt) {
    opt.addEventListener('click', function () {
      opts.forEach(function (o) {
        var active = o === opt;
        o.classList.toggle('is-active', active);
        o.setAttribute('aria-checked', String(active));
      });
      document.documentElement.dataset.contrast = opt.dataset.level;
    });
  });
})();
CSS
.nuda-tt2-contrast {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 10px;
}

.nuda-tt2-contrast__opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 44px;
  min-height: 44px;
  padding: 6px 8px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: #a1a1aa;
  cursor: pointer;
  transition: background .25s,border-color .25s,color .25s;
}

.nuda-tt2-contrast__opt:focus-visible {
  outline: 2px solid #e4ff54;
  outline-offset: 2px;
}

.nuda-tt2-contrast__opt.is-active {
  background: rgba(228,255,84,.1);
  border-color: rgba(228,255,84,.3);
  color: #e4ff54;
}

.nuda-tt2-contrast__bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 12px;
}

.nuda-tt2-contrast__bar {
  width: 3px;
  height: 100%;
  border-radius: 1px;
  background: currentColor;
  opacity: .25;
  transform: scaleY(.4);
  transform-origin: bottom;
  transition: opacity .3s,transform .3s;
}

.nuda-tt2-contrast__opt[data-level="normal"] .nuda-tt2-contrast__bar:first-child {
  opacity: 1;
  transform: scaleY(.4);
}

.nuda-tt2-contrast__opt[data-level="high"] .nuda-tt2-contrast__bar:nth-child(1) {
  opacity: 1;
  transform: scaleY(.6);
}

.nuda-tt2-contrast__opt[data-level="high"] .nuda-tt2-contrast__bar:nth-child(2) {
  opacity: 1;
  transform: scaleY(.8);
}

.nuda-tt2-contrast__opt[data-level="max"] .nuda-tt2-contrast__bar {
  opacity: 1;
}

.nuda-tt2-contrast__opt[data-level="max"] .nuda-tt2-contrast__bar:nth-child(1) {
  transform: scaleY(.6);
}

.nuda-tt2-contrast__opt[data-level="max"] .nuda-tt2-contrast__bar:nth-child(2) {
  transform: scaleY(.8);
}

.nuda-tt2-contrast__opt[data-level="max"] .nuda-tt2-contrast__bar:nth-child(3) {
  transform: scaleY(1);
}

.nuda-tt2-contrast__label {
  font: 600 9px ui-sans-serif,system-ui;
  letter-spacing: .02em;
}

@media (prefers-reduced-motion:reduce) {
  .nuda-tt2-contrast__opt,.nuda-tt2-contrast__bar {
    transition: none;
  }
}

How to use Contrast Stepper

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