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
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.