Pure CSS toggle switch with two labels

For an upcoming web project, I needed a switch which chose between two options. Not “on or off”, like a checkbox; more “tea or coffee”, one choice or the other. That could indeed be a checkbox (checked means “tea”, unchecked means “coffee”) but that’s not really right; it’s, semantically, two radio buttons. But visually it was meant to look like a toggle switch with two labels: “tea” and “coffee”, and a switch to switch between them.

So, a bit of CSS trickery and it all works out. Evolving step by step:

First, start with just the plain HTML we want. Two radio buttons, each with a label. Note that clicking the label sets the radio button to checked, as you’d expect.

<input type="radio" id="radiocoffee" name="drink" checked><label 
for="radiocoffee">Coffee</label><input type="radio" id="radiotea" 
name="drink"><label for="radiotea">Tea</label>

We need the radio buttons because they do the work, and we need them semantically, but we don’t want to see them. Hide them away.

input { position: absolute; opacity: 0; }

Now, we need somewhere to put the body of our switch. Take the left-hand label, and using CSS generated content, reserve a block of space to put our switch in. (Note: we also temporarily colour various parts for this demo and offset them, so you can see what’s going on.)

input + label::after {
    content: ".";
    width: 150px;
    display: inline-block;
    text-indent: -1000em;

/* We only want this on the first label, so hide it on the second! */
input + label + input + label::after {
    width: 0;
    border-width: 0;

Now, the switch part of the switch needs to go somewhere. Reserve some space on the left of the second label to put that switch part in.

input + label + input + label {
    padding-right: 0;
    padding-left: 150px;
    margin-left: -150px;

Actually draw the switch body, with a border and border-radius. This is why the space for the switch body was reserved with ::after generated content, rather than right padding on the label — so that the switch body can have its own border and styles.

input + label::after {
    border: 2px solid black;
    border-radius: 600px;

Draw the switch part of the switch as a circle, by using a radial gradient on the background of the right-hand label. Obviously you’ll need to add vendor prefixes to radial-gradient, or use Lea Verou’s prefixfree or similar.

input + label + input + label {
    background-image: radial-gradient(circle, transparent 0%, transparent 50%, black 50%, black 54%, transparent 59%, transparent 100%);
    background-position: 0px -1px;
    background-size: 40px 105%;
    background-repeat: no-repeat;

(If you’re not sure precisely how that circle gets drawn by a radial gradient, it’s morally similar to a trick with using gradients to make patterns (which Lea Verou has been doing for ages) and the basic principle (make some parts transparent, some parts a colour) dates back to Tantek Çelik’s A Study of Regular Polygons, which was twelve and a half years ago (blimey). Here’s the same gradient with only the colours changed, which might make it more obvious how it works.)

Remove our deliberate misalignment so everything lines up properly:

Right! Now it looks like we wanted. (Well, it doesn’t; it obviously looks plain, black-and-white, and boring. Making it look swishy and in keeping with your own beautiful designs is left as an exercise for the reader; use whichever cool CSS3 border tricks and gradients you want.) However, it doesn’t do anything, yet. As you saw on the very first example, clicking the labels does toggle the radio button correctly, but we’re not seeing that here. What we want is to change the position of the switch when the radio button is toggled, and for that there’s another fairly standard trick, using CSS’s :checked pseudo-class and + adjacent-sibling selectors. Basically, if you’ve got a radio button or checkbox, and you put its label immediately after it in the source, then input:checked + label will select that label for styling, only if the checkbox is checked! And clicking the label toggles the checkbox. So you get the visual effect that clicking the label toggles the appearance of the label itself, but it’s all using decent semantic HTML so it doesn’t need JavaScript, it all ought to be accessible correctly because it’s decent semantic HTML, and you don’t have to do any coding at all because the browser does it all for you. Dolla, dolla bills, y’all.

All we do is decrease the size of the reserved space for the switch part of the switch…which has the visual effect of moving it to the right, closer to its label. Click the switch below and you’ll see it working.

input + label + input:checked + label {
    padding-left: 40px;
    margin-left: -40px;

It’d be nice to indicate which label is selected, too; here, we colour it red:

input:checked + label { color: red; }

And finally, the switch change ought to animate, which is simply doable with CSS transitions (which, again, may need to be vendor prefixed).

input + label + input + label { transition: all 0.3s ease; }

And that’s the whole thing. It glues together a few different things from various places, but none of this is especially complex, and it’s all long-existing, well-supported (by modern browsers) CSS. It’s often a bit counter-intuitive, this sort of thing, but it ought to be possible, most of the time, to achieve the visual look you want without having to resort to JS, without having to give up on the underlying HTML, and without any images. I like that. Actual code all in one place at http://jsbin.com/ofadon/2/edit or in a gist at https://gist.github.com/stuartlangridge/5691475.

I'm currently available for hire, to help you plan, architect, and build new systems, and for technical writing and articles. You can take a look at some projects I've worked on and some of my writing. If you'd like to talk about your upcoming project, do get in touch.

More in the discussion (powered by webmentions)