47 CSS Toggles & Switches35 / 47
Material Design
Google Material Design 3 switch — track with rounded thumb that lifts and changes colour on toggle. Active state shows a filled icon-style indicator.
Published Updated
The code
<label class="tgl-35">
<input class="tgl-35__input" type="checkbox" checked>
<span class="tgl-35__track" aria-hidden="true">
<span class="tgl-35__thumb"></span>
</span>
<span class="tgl-35__label">Notifications</span>
</label><label class="tgl-35">
<input class="tgl-35__input" type="checkbox" checked>
<span class="tgl-35__track" aria-hidden="true">
<span class="tgl-35__thumb"></span>
</span>
<span class="tgl-35__label">Notifications</span>
</label>.tgl-35 {
font-family: "Roboto", "Google Sans", sans-serif;
font-size: 14px;
color: #e6e0e9;
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 48px 20px;
background: #14141e;
box-sizing: border-box;
}
.tgl-35__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.tgl-35__track {
position: relative;
width: 52px;
height: 32px;
background: #49454f;
border: 2px solid #79747e;
border-radius: 999px;
transition: background 0.2s ease, border-color 0.2s ease;
box-sizing: border-box;
}
.tgl-35__thumb {
position: absolute;
top: 50%;
left: 6px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #cac4d0;
transform: translateY(-50%);
transition: left 0.2s cubic-bezier(0.4, 0, 0.2, 1), width 0.15s ease, height 0.15s ease, background 0.2s ease;
}
.tgl-35__input:checked ~ .tgl-35__track {
background: #d0bcff;
border-color: #d0bcff;
}
.tgl-35__input:checked ~ .tgl-35__track .tgl-35__thumb {
/* Inner width is 48px (52 - 2px border × 2). Thumb grows to 24px
on-state, so max left value that keeps the thumb fully inside the
border is 48 - 24 = 24px, minus 2px breathing room = 22px. */
left: 22px;
width: 24px;
height: 24px;
background: #381e72;
}
.tgl-35__input:focus-visible ~ .tgl-35__track {
outline: 2px solid #d0bcff;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.tgl-35__track,
.tgl-35__thumb {
transition: none;
}
}.tgl-35 {
font-family: "Roboto", "Google Sans", sans-serif;
font-size: 14px;
color: #e6e0e9;
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 48px 20px;
background: #14141e;
box-sizing: border-box;
}
.tgl-35__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.tgl-35__track {
position: relative;
width: 52px;
height: 32px;
background: #49454f;
border: 2px solid #79747e;
border-radius: 999px;
transition: background 0.2s ease, border-color 0.2s ease;
box-sizing: border-box;
}
.tgl-35__thumb {
position: absolute;
top: 50%;
left: 6px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #cac4d0;
transform: translateY(-50%);
transition: left 0.2s cubic-bezier(0.4, 0, 0.2, 1), width 0.15s ease, height 0.15s ease, background 0.2s ease;
}
.tgl-35__input:checked ~ .tgl-35__track {
background: #d0bcff;
border-color: #d0bcff;
}
.tgl-35__input:checked ~ .tgl-35__track .tgl-35__thumb {
/* Inner width is 48px (52 - 2px border × 2). Thumb grows to 24px
on-state, so max left value that keeps the thumb fully inside the
border is 48 - 24 = 24px, minus 2px breathing room = 22px. */
left: 22px;
width: 24px;
height: 24px;
background: #381e72;
}
.tgl-35__input:focus-visible ~ .tgl-35__track {
outline: 2px solid #d0bcff;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.tgl-35__track,
.tgl-35__thumb {
transition: none;
}
}/* No JavaScript — the M3 thumb grows 16→24px and shifts left:6→22px on separate transitions while track and border merge to the tonal fill. */
/* No JavaScript — the M3 thumb grows 16→24px and shifts left:6→22px on separate transitions while track and border merge to the tonal fill. */Here's a working CSS Toggles & Switche from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Material Design
Source: https://codefronts.com/components/css-toggles-switches/material-design/
Google Material Design 3 switch — track with rounded thumb that lifts and changes colour on toggle. Active state shows a filled icon-style indicator.
## HTML
```html
<label class="tgl-35">
<input class="tgl-35__input" type="checkbox" checked>
<span class="tgl-35__track" aria-hidden="true">
<span class="tgl-35__thumb"></span>
</span>
<span class="tgl-35__label">Notifications</span>
</label>
```
## CSS
```css
.tgl-35 {
font-family: "Roboto", "Google Sans", sans-serif;
font-size: 14px;
color: #e6e0e9;
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 48px 20px;
background: #14141e;
box-sizing: border-box;
}
.tgl-35__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.tgl-35__track {
position: relative;
width: 52px;
height: 32px;
background: #49454f;
border: 2px solid #79747e;
border-radius: 999px;
transition: background 0.2s ease, border-color 0.2s ease;
box-sizing: border-box;
}
.tgl-35__thumb {
position: absolute;
top: 50%;
left: 6px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #cac4d0;
transform: translateY(-50%);
transition: left 0.2s cubic-bezier(0.4, 0, 0.2, 1), width 0.15s ease, height 0.15s ease, background 0.2s ease;
}
.tgl-35__input:checked ~ .tgl-35__track {
background: #d0bcff;
border-color: #d0bcff;
}
.tgl-35__input:checked ~ .tgl-35__track .tgl-35__thumb {
/* Inner width is 48px (52 - 2px border × 2). Thumb grows to 24px
on-state, so max left value that keeps the thumb fully inside the
border is 48 - 24 = 24px, minus 2px breathing room = 22px. */
left: 22px;
width: 24px;
height: 24px;
background: #381e72;
}
.tgl-35__input:focus-visible ~ .tgl-35__track {
outline: 2px solid #d0bcff;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.tgl-35__track,
.tgl-35__thumb {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — the M3 thumb grows 16→24px and shifts left:6→22px on separate transitions while track and border merge to the tonal fill. */
```Here's a working CSS Toggles & Switche from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Material Design
Source: https://codefronts.com/components/css-toggles-switches/material-design/
Google Material Design 3 switch — track with rounded thumb that lifts and changes colour on toggle. Active state shows a filled icon-style indicator.
## HTML
```html
<label class="tgl-35">
<input class="tgl-35__input" type="checkbox" checked>
<span class="tgl-35__track" aria-hidden="true">
<span class="tgl-35__thumb"></span>
</span>
<span class="tgl-35__label">Notifications</span>
</label>
```
## CSS
```css
.tgl-35 {
font-family: "Roboto", "Google Sans", sans-serif;
font-size: 14px;
color: #e6e0e9;
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 48px 20px;
background: #14141e;
box-sizing: border-box;
}
.tgl-35__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.tgl-35__track {
position: relative;
width: 52px;
height: 32px;
background: #49454f;
border: 2px solid #79747e;
border-radius: 999px;
transition: background 0.2s ease, border-color 0.2s ease;
box-sizing: border-box;
}
.tgl-35__thumb {
position: absolute;
top: 50%;
left: 6px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #cac4d0;
transform: translateY(-50%);
transition: left 0.2s cubic-bezier(0.4, 0, 0.2, 1), width 0.15s ease, height 0.15s ease, background 0.2s ease;
}
.tgl-35__input:checked ~ .tgl-35__track {
background: #d0bcff;
border-color: #d0bcff;
}
.tgl-35__input:checked ~ .tgl-35__track .tgl-35__thumb {
/* Inner width is 48px (52 - 2px border × 2). Thumb grows to 24px
on-state, so max left value that keeps the thumb fully inside the
border is 48 - 24 = 24px, minus 2px breathing room = 22px. */
left: 22px;
width: 24px;
height: 24px;
background: #381e72;
}
.tgl-35__input:focus-visible ~ .tgl-35__track {
outline: 2px solid #d0bcff;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.tgl-35__track,
.tgl-35__thumb {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — the M3 thumb grows 16→24px and shifts left:6→22px on separate transitions while track and border merge to the tonal fill. */
```How this works
M3's defining trait is the state-carrying thumb: unchecked it's a small 16px dot in outline color resting 6px from the left of the 52×32 outlined track; checked it grows to 24px, deepens to the on-primary tone (#381e72 on a #d0bcff track) and moves to left:22px — the exact spec sum (48px inner − 24px thumb − 2px breathing room).
Growth and travel are separate transitions (width/height at 0.15s, left at 0.2s on the standard curve) so the thumb visibly swells as it arrives, matching the Material motion spec's emphasis. Track fill and border collapse to the same tonal color when checked, per M3.
Make it yours
- Retheme with your own M3 tonal pair (track/thumb) — the geometry is spec-fixed.
- Add the pressed 28px state with :active width/height rules.
- Drop in a checkmark icon via a masked ::after (see demo 14).
- Light-scheme variant: swap the four color values only.
Gotchas — read before shipping
- The left-position math must track any resize: inner width − thumb − 2px; hardcoded 22px only works at spec size.
- This variant animates width/height/left for spec fidelity on a 24px element — negligible, but demo 14 shows the transform-only approach if you're INP-strict.
- Keep the 2px outline in the off state — it's M3's required affordance that the control is interactive.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 26+ | 9+ | 16+ | 26+ |
Plain transitions + sibling :checked; universal in modern browsers.