Library / Buttons

Rocker switch

Price: Free

On or off. A real checkbox with the switch role, so keyboards and screen readers already know it. It lights from inside when on. Used as a link, it stays lit and rocks off when pressed.

IOGet started

Copy the CSS once and the markup wherever you need it. Set --c to change the color and --ink for the label; all the shading follows from those. It uses system fonts, so it needs nothing from us; set --btn-font to use your own. Covered by the Atomic Socket License.

React
{/* a switch */}
<label className="rocker" style={{ "--c": "#19b56f" } as React.CSSProperties}>
  <input type="checkbox" role="switch" defaultChecked />
  <span className="rocker-bezel">
    <span className="rocker-well">
      <span className="rocker-body">
        <span className="rocker-face rocker-i">I</span>
        <span className="rocker-face rocker-o">O</span>
      </span>
    </span>
  </span>
  <span>Dark mode</span>
</label>

{/* a link */}
<a className="rocker" href="/start" style={{ "--c": "#2348d8" } as React.CSSProperties}>
  <span className="rocker-bezel">
    <span className="rocker-well">
      <span className="rocker-body">
        <span className="rocker-face rocker-i">I</span>
        <span className="rocker-face rocker-o">O</span>
      </span>
    </span>
  </span>
  <span>Get started</span>
</a>
HTML
<!-- a switch -->
<label class="rocker" style="--c:#19b56f">
  <input type="checkbox" role="switch" checked>
  <span class="rocker-bezel">
    <span class="rocker-well">
      <span class="rocker-body">
        <span class="rocker-face rocker-i">I</span>
        <span class="rocker-face rocker-o">O</span>
      </span>
    </span>
  </span>
  <span>Dark mode</span>
</label>

<!-- a link -->
<a class="rocker" href="/start" style="--c:#2348d8">
  <span class="rocker-bezel">
    <span class="rocker-well">
      <span class="rocker-body">
        <span class="rocker-face rocker-i">I</span>
        <span class="rocker-face rocker-o">O</span>
      </span>
    </span>
  </span>
  <span>Get started</span>
</a>
CSS
/* Buttons by EELUMA: eeluma.com/library/buttons */
.keycap, .keycap *, .rocker, .rocker *, .arcade, .arcade * { box-sizing: border-box; }

/* Rocker switch: on or off. It rocks over and lights from inside. As a link it stays lit and rocks off when pressed */
.rocker {
  --c: #e8341f;
  position: relative; display: inline-flex; align-items: center; gap: 12px; cursor: pointer;
  font: 500 12px/1.2 var(--btn-font, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
  letter-spacing: .06em; text-transform: uppercase; -webkit-tap-highlight-color: transparent;
}
a.rocker { color: inherit; text-decoration: none; }
.rocker input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }
.rocker-bezel {
  display: block; width: 50px; height: 78px; padding: 5px; border-radius: 7px; perspective: 150px;
  background: linear-gradient(160deg, #42464a, #141618 46%, #2b2e31);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 -1px 0 rgba(0,0,0,.8), 0 1px 1px rgba(0,0,0,.5), 0 7px 11px -4px rgba(0,0,0,.55);
  transition: box-shadow 120ms ease;
}
.rocker-well { display: block; height: 100%; padding: 1px; border-radius: 3px; background: #030404; box-shadow: inset 0 1px 3px #000; }
.rocker-body {
  display: flex; flex-direction: column; height: 100%; border-radius: 2px; overflow: hidden;
  transform-style: preserve-3d; transform: rotateX(-11deg);
  transition: transform 130ms cubic-bezier(.3,1.9,.5,1);
}
.rocker-face {
  flex: 1; display: flex; align-items: center; justify-content: center;
  font: 700 13px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
  transition: background 90ms linear, color 90ms linear, text-shadow 90ms linear, box-shadow 110ms linear;
}
/* Off: the O half is pushed in and the unlit I half stands proud */
.rocker-i { color: rgba(255,255,255,.3); background: linear-gradient(180deg, color-mix(in oklab, var(--c), black 40%), color-mix(in oklab, var(--c), black 62%)); box-shadow: inset 0 1px 0 rgba(255,255,255,.28); }
.rocker-o { color: rgba(255,255,255,.34); background: linear-gradient(180deg, color-mix(in oklab, var(--c), black 82%), color-mix(in oklab, var(--c), black 70%)); box-shadow: inset 0 2px 3px rgba(0,0,0,.6); }
/* On: it rocks over and the I half lights up */
.rocker input:checked ~ .rocker-bezel .rocker-body, a.rocker:not(:active) .rocker-bezel .rocker-body { transform: rotateX(11deg); }
.rocker input:checked ~ .rocker-bezel .rocker-i, a.rocker:not(:active) .rocker-bezel .rocker-i { color: #fff; text-shadow: 0 0 7px #fff, 0 0 3px #fff; background: radial-gradient(90% 90% at 50% 62%, color-mix(in oklab, var(--c), white 40%), var(--c) 72%); box-shadow: inset 0 3px 4px color-mix(in oklab, var(--c), black 45%); }
.rocker input:checked ~ .rocker-bezel .rocker-o, a.rocker:not(:active) .rocker-bezel .rocker-o { color: rgba(255,255,255,.72); background: linear-gradient(180deg, var(--c), color-mix(in oklab, var(--c), black 35%)); box-shadow: inset 0 -1px 0 rgba(255,255,255,.3); }
.rocker input:checked ~ .rocker-bezel, a.rocker:not(:active) .rocker-bezel { box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 -1px 0 rgba(0,0,0,.8), 0 1px 1px rgba(0,0,0,.5), 0 7px 11px -4px rgba(0,0,0,.55), 0 0 20px -2px color-mix(in oklab, var(--c), transparent 42%); }
.rocker input:focus-visible ~ .rocker-bezel, a.rocker:focus-visible .rocker-bezel { outline: 2px solid var(--btn-focus, #2d6cdf); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .rocker-bezel, .rocker-body, .rocker-face { transition: none; } }