4 toggle switch designs in pure CSS

Four switches, each a real checkbox styled with CSS. They work with a keyboard and a screen reader with no extra code.

UI loop

4 toggle switches

Sundry Millsundrymill.com/library
4 toggle switch designs in pure CSS

Everything in the preview is live. Try it.

The code, one by one

Shared: the row around each switch
.tg-row {
  display: flex;
  align-items: center;
  gap: 12px;
  font-weight: 600;
  cursor: pointer;
}

Classic

The phone settings switch everyone already knows.

HTML
<label class="tg-row"><input class="tg-pill" type="checkbox" role="switch" checked> Wi-Fi</label>
CSS
.tg-pill {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 52px;
  height: 32px;
  margin: 0;
  border-radius: 999px;
  background: #8A929E;
  cursor: pointer;
  transition: background-color .25s;
}
.tg-pill::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .25);
  transition: transform .3s cubic-bezier(.05, .7, .1, 1);
}
.tg-pill:checked { background: #1E8A50; }
.tg-pill:checked::before { transform: translateX(20px); }

Day and night

A sun that turns into a moon, with stars coming out.

HTML
<label class="tg-row"><input class="tg-sky" type="checkbox" role="switch" checked> Night</label>
CSS
.tg-sky {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 64px;
  height: 34px;
  margin: 0;
  border-radius: 999px;
  background: #7CC8F2;
  overflow: hidden;
  cursor: pointer;
  transition: background-color .4s;
}
.tg-sky::before {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #FFC53D;
  box-shadow: inset 0 0 0 0 #1B2447;
  transition: transform .45s cubic-bezier(.05, .7, .1, 1), background-color .45s, box-shadow .45s;
}
.tg-sky::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 18px 10px, #fff 1px, transparent 1.6px),
    radial-gradient(circle at 26px 22px, #fff 1px, transparent 1.6px),
    radial-gradient(circle at 12px 24px, #fff .8px, transparent 1.4px);
  opacity: 0;
  transition: opacity .45s;
}
.tg-sky:checked { background: #1B2447; }
.tg-sky:checked::before {
  transform: translateX(30px);
  background: #F2EADC;
  box-shadow: inset -7px -3px 0 0 #C9C1B4;
}
.tg-sky:checked::after { opacity: 1; }

Stretchy

The knob stretches while you hold it, then snaps across.

HTML
<label class="tg-row"><input class="tg-squish" type="checkbox" role="switch"> Dark mode</label>
CSS
.tg-squish {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 56px;
  height: 32px;
  margin: 0;
  border-radius: 999px;
  background: #D9D3C7;
  cursor: pointer;
  transition: background-color .25s;
}
.tg-squish::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: #17202C;
  transition: width .2s cubic-bezier(.05, .7, .1, 1), transform .3s cubic-bezier(.05, .7, .1, 1);
}
.tg-squish:active::before { width: 34px; }
.tg-squish:checked { background: #17202C; }
.tg-squish:checked::before { background: #FDF9F1; transform: translateX(24px); }
.tg-squish:checked:active::before { transform: translateX(16px); }

Tick and cross

The knob carries a cross when off and a tick when on, so the state never depends on colour alone.

HTML
<label class="tg-row"><input class="tg-tick" type="checkbox" role="switch" checked> Notifications</label>
CSS
.tg-tick {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 60px;
  height: 34px;
  margin: 0;
  border-radius: 999px;
  background: #B9B2A6;
  cursor: pointer;
  transition: background-color .3s;
}
.tg-tick::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #FFFFFF url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23B4553A' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M7 7l10 10M17 7L7 17'/%3E%3C/svg%3E") center / 16px no-repeat;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .25);
  transition: transform .3s cubic-bezier(.05, .7, .1, 1);
}
.tg-tick:checked { background: #1E8A50; }
.tg-tick:checked::before {
  transform: translateX(26px);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231E8A50' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E");
}

How to use it

  • role="switch" tells screen readers it is on or off, not ticked or unticked. The label around it gives it a name.
  • A switch should act at once, like a light. If the change needs a Save button, use a checkbox instead.
  • Keep the off state visible against your background: the track needs 3 to 1 contrast with what is around it.

Showing an interaction like this to a client or in a tutorial? Framok records your screen and zooms in on every click for you.

Free to use in personal and commercial work. No credit needed, though a link back is always nice.

More from the library

See the whole library