Pure CSS Buttons & forms #form-hack #ui #controls

Rocker switch in pure CSS

A real checkbox underneath: click, tap or press Space. The rocker tips on :checked.

  • 58 lines of CSS
  • 5 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. A real <input type="checkbox"> holds the state, so keyboard, forms and screen readers all work.
  2. It is visually hidden (not display: none, which would remove it from the tab order).
  3. The rocker is the next sibling: input:checked + .rocker tips it from rotateX(-22deg) to rotateX(22deg).
  4. Wrapping everything in a <label> makes the whole switch clickable with zero JavaScript.

Key techniques

  • input:checked + sibling
  • rotateX rocker
  • label makes the whole thing clickable

Copy-paste code

HTML 5 lines

<label class="switch">
  <input type="checkbox">
  <span class="rocker"><b>I</b><b>O</b></span>
  <em></em>
</label>

CSS 58 lines

.switch {
  display: grid;
  justify-items: center;
  gap: 22px;
  perspective: 320px;
  cursor: pointer;
}

/* hidden but still focusable */
.switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.rocker {
  display: grid;
  grid-template-rows: 1fr 1fr;
  width: 90px;
  height: 146px;
  border-radius: 14px;
  background: linear-gradient(#2b3050, #161a30);
  border: 1px solid #4a5280;
  color: #8d95b3;
  font: 700 1.6rem system-ui;
  text-align: center;
  box-shadow: 0 0 0 9px #05060c, 0 0 0 10px #4a5280;
  transform: rotateX(-22deg);                       /* OFF */
  transition: transform 0.18s cubic-bezier(0.3, 1.6, 0.5, 1);
}

.rocker b { display: grid; place-items: center; }

input:checked + .rocker {
  transform: rotateX(22deg);                        /* ON */
  color: #2ee6d6;
}

input:focus-visible + .rocker {
  outline: 2px solid #2ee6d6;
  outline-offset: 14px;
}

/* status LED */
.switch em {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #3a3f5c;
  transition: 0.2s;
}

input:checked ~ em {
  background: #2ee6d6;
  box-shadow: 0 0 14px #2ee6d6;
}

Sass source 56 lines

.d-switch {
  display: grid;
  justify-items: center;
  gap: 18px;
  perspective: 320px;
  cursor: pointer;

  &__rocker {
    display: grid;
    grid-template-rows: 1fr 1fr;
    width: 74px;
    height: 120px;
    border-radius: 12px;
    background: linear-gradient(#2b3050, #161a30);
    border: 1px solid var(--border-strong);
    color: #8d95b3;
    font-size: 22px;
    text-align: center;
    box-shadow:
      0 0 0 8px #0b0d18,
      0 0 0 9px var(--border-strong);
    // OFF: bottom half pressed in
    transform: rotateX(-22deg);
    transition: transform 0.18s cubic-bezier(0.3, 1.6, 0.5, 1);

    b {
      display: grid;
      place-items: center;
    }
  }

  // status LED
  em {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #3a3f5c;
    transition: 0.2s;
  }

  // ON: top half pressed in
  input:checked + &__rocker {
    transform: rotateX(22deg);
    color: var(--accent-2);
  }

  input:checked ~ em {
    background: var(--accent-2);
    box-shadow: 0 0 14px var(--accent-2);
  }

  input:focus-visible + &__rocker {
    outline: 2px solid var(--accent-2);
    outline-offset: 12px;
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.