- ** Daily Tips **
- December 10, 2025
Today, I wanted to quickly share how I built Kelp’s toggle switch component with a single HTML attribute and CSS (no JavaScript required).
Let’s dig in!
The [role="switch"] attribute
The [role="switch"] attribute is functionality identical to [role="checkbox"] (the implicit role a [type="checkbox"] element has), except that it conveys an on/off state instead of checked/unchecked.
<label for="agree">
<input
type="checkbox"
id="agree"
>
I'm a standard checkbox
</label>
<label for="switch">
<input
type="checkbox"
id="switch"
role="switch"
>
I'm a switch
</label>
On it’s own, it changes nothing about how a checkbox looks or behaves.
I’m …
- ** Daily Tips **
- December 10, 2025
Today, I wanted to quickly share how I built Kelp’s toggle switch component with a single HTML attribute and CSS (no JavaScript required).
Let’s dig in!
The [role="switch"] attribute
The [role="switch"] attribute is functionality identical to [role="checkbox"] (the implicit role a [type="checkbox"] element has), except that it conveys an on/off state instead of checked/unchecked.
<label for="agree">
<input
type="checkbox"
id="agree"
>
I'm a standard checkbox
</label>
<label for="switch">
<input
type="checkbox"
id="switch"
role="switch"
>
I'm a switch
</label>
On it’s own, it changes nothing about how a checkbox looks or behaves.
I’m a standard checkbox
I’m a switch
We can hook into it with CSS to style our switch differently.
It already provides the semantics we need for screen readers, and checkboxes already have the keyboard interaction behaviors we want.
Styling a switch
You can style attributes the same way you’d style elements, IDs, and classes by wrapping your selector in square brackets ([]).
Let’s start by styling our base “off” switch.
First, we need to remove the default browser styles, which we can do with appearance: none. We’ll also add display: inline-block so that we can style it as a block element while keeping it inline with the flow.
[role="switch"] {
appearance: none;
display: inline-block;
}
We want our switch to be a bit wider than it is tall. We’re going to use those values a few times, so we’ll save them to CSS variables.
We can use our --height outright. We’ll use the --width as a ratio relative to the --height.
The CSS calc() function (yes, CSS is a programming language and has functions) to add 1 to our --width and then multiply it by the --height.
This creates a 1.8:1 ratio of width:height.
[role="switch"] {
--height: 1.1875em;
--width: 0.8;
appearance: none;
display: inline-block;
height: var(--height);
width: calc(var(--height) * calc(1 + var(--width)));
}
Finally, let’s add a few visual details.
We’ll remove the border, and set a border-radius of 99em to give the edges a curved or “pill” appearance.
[role="switch"] {
--height: 1.1875em;
--width: 0.8;
appearance: none;
display: inline-block;
background-color: #e5e5e5;
height: var(--height);
width: calc(var(--height) * calc(1 + var(--width)));
border: 0;
border-radius: 99em;
}
Aligning things
For all of this to work properly, we also need to set box-sizing to border-box.
As a general rule, I like to apply that to everything in my designs, but you could apply it to just the [role="switch"] if you wanted.
/**
* Add box sizing to everything
* @link http://www.paulirish.com/2012/box-sizing-border-box-ftw/
*/
*,
*:before,
*:after {
box-sizing: border-box;
}
We’ll also use the :has() CSS pseudo-class to check if our label has a [role="switch"] checkbox in it.
If so, we’ll apply display: inline-flex to our label so that we can center the toggle switch and label text, and give it a small gap.
label:has([role="switch"]) {
display: inline-flex;
align-items: center;
gap: 0.25em;
}

It doesn’t look like much, but we’ll get there soon!
Adding the actual toggle switch
The reason this doesn’t look like much of anything is because it doesn’t have the actual toggle switch yet.
We’ll add that using the ::after pseudo-element. The most important parts here are to assign:
- A
contentproperty with an empty string ("") so that it’s actually displayed. - An
aspect-ratioof1, so that toggle has the same height and width. - A border-radius of
50%to make it round. - A height of
100%so it fills theheightof the toggle.
/* thumb */
[role="switch"]::after {
content: "";
display: block;
aspect-ratio: 1;
height: 100%;
border-radius: 50%;
}
When switches are poorly styled, it can be difficult to tell when they’re “on” or “off.”
To make it nice and clear, we’ll use a very muted color with dark border in the “off” position: white with dark gray. I’m using the max() function to use 2px or 0.125em for the border width, whichever is larger.
[role="switch"]::after {
content: "";
display: block;
aspect-ratio: 1;
background-color: white;
border: max(2px, 0.125em) solid #808080;
height: 100%;
border-radius: 50%;
}
In the “on” position, we’ll make the toggle a lot more vivid.

Now, this is visibly a toggle. Let’s add the “on”/“off” state!
Toggle switch on/off state
Under-the-hood, our switch is a checkbox. That means it has the :checked state when it’s “on.”
We’ll add a bright blue background to the :checked state for the [role="switch"] element. We’ll also use that same color for the ::after pseudo-element (the toggle) border-color.
[role="switch"]:checked {
background-color: #0088cc;
}
[role="switch"]:checked::after {
border-color: #0088cc;
}
We also want the toggle switch to slide over to the right. We can do that by adding a translate property.
We’ll use calc() to get the get the width of our toggle, but we’ll leave out the + 1 to account for the size of our toggle switch. Then, we’ll slide the toggle over by that much.
[role="switch"]:checked::after {
border-color: #0088cc;
translate: calc(var(--height) * var(--width)) 0;
}
Now, toggling it “on” and “off” will change the color and move the toggle.

Adding a subtle animation
This is totally optional, but we can animate the toggle slide and background color with CSS, too.
We’ll add a transition to the background-color on our [role="switch"] element.
[role="switch"] {
/* ... */
transition: background-color 100ms ease-in-out;
}
And we’ll add a transition to the translate on the toggle itself.
[role="switch"]::after {
/* ... */
transition: translate 100ms ease-in-out;
}
Now, the switch slides when you toggle it.
Accessibility considerations
The technical accessibility considerations are taken care of with [role="switch"] and style choices.
But how and when you use a toggle over a regular checkbox is important, too. The intent here is to convey on/off state.
Do not use this component for things like accepting terms of service or selecting for a list of items. Do use it for turning features or settings on or off.
The label text should also make it clear that you’re turning something on/off.
<label for="dark-mode">
<input role="switch" id="dark-mode">
Enable Dark Mode
</label>
And the label text should be on the “on” side for the switch.
Check out Kelp!
If you want components like this and many more in an extremely customizable and lightweight UI library, checkout Kelp UI.
Kelp is a UI library for people who love HTML, powered by modern CSS and Web Components. It’s written with vanilla CSS and JavaScript, and is built to be easily customized with no build steps.