Button Switches with Checkboxes and CSS3 Fanciness

In this tutorial we will create some realistic-looking switch buttons using pseudo-elements and checkboxes.

Button Switches with Checkboxes and CSS3 Fanciness

Hello everyone! For a while now, I’m having fun with the new CSS properties (CSS3). It’s an incredible playground for creativity and cleverness. I particularly enjoy designing and creating UI elements like buttons, switches, toggles and such. That’s the focus of this tutorial, I’ll show you how to create button switches with CSS only, without a line of JavaScript. Ready for it?
A few things before starting:

  • You won’t see any vendor prefixes in the CSS snippets, but you will of course find them in the files.
  • We won’t use much transitions between states because we often use pseudo-elements to reduce the number of elements to its minimum, and as of writing, only Firefox supports transitions/animations on pseudo-elements.
  • We’ll use the “checkbox hack” (see below).
  • I personally use the box-model where [width] = [element-width] + [padding] + [borders]. I activate it with the following snippet:
    *,
    *:after,
    *:before {
        box-sizing: border-box;
    }

About the “Checkbox Hack”

The checkbox hack allows us to have a toggle handler in pure CSS. It relies on a checkbox (which is either checked or unchecked), the :checked pseudo-selector, and a sibling selector (~ or +). Basically, you can say “if the checkbox is checked, then the following X element is behaving like this”.
The old way consisted of having a checkbox with an ID, and a label tag with a “for” attribute calling the checkbox ID. This technique allowed to hide the checkbox and still toggle it by clicking on the label. The only problem with this approach it that mobile Safari doesn’t support it.
So we have a workaround for that: turn the checkbox invisible with the opacity property, set it on top of whatever element you want (I still use label) and there you go, when clicking the element, you are actually clicking the checkbox! The code looks like this:

.switch input {
    /* First, we make it as wide as the container */
    position: absolute;
    width: 100%;
    height: 100%;
    /* Then, we put it on top of everything else */
    z-index: 100;
    /* Last, we make it invisible */
    opacity: 0;
    /* This one is just for ergonomy */
    cursor: pointer;
}

Those 12 lines of CSS will be used in all the following demos, so feel free to copy paste it; it never changes.
Note: for another awesome demo including the checkbox hack, you may want to have a look at this tutorial: Annotation overlay effect with CSS3.

Example 1

Let’s start with a lovely little push button, with a small illuminated LED when it’s on.

The Markup

<div class="switch">
	<input type="checkbox">
	<label></label>
</div> 

The .switch element is our container. Within this very element the checkbox and the label will be positioned on top of each other to allow us to do the magic trick.
The input doesn’t need anything more than the checkbox type (no ID, no name, nothing).

And the last element could be anything (an anchor, a div, a span, a label, a bold/italic tag, whatever). I personally chose a label because I used to use the old checkbox hack, which required a label.

The CSS

First, we give our button a size by setting the width and height to the container. Be sure the width and the height are the same, or your button won’t be round. Remember to hide the checkbox with the according CSS code at the top of this article.

.switch {
    width: 100px;
    height: 100px;
    position: relative;
}

Then, the label, which is where we’ll add all the styling to. First of all, we set it to block and make it 100% of its parent size (the container). Then, we give it a relative position in order to add some pseudo-elements later.

Time for some real styles: a background color, a round shape, and some awesome box-shadows to make it look like it’s embossed.

.switch label {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    border-radius: 50%;
    background: #eaeaea;
    box-shadow: 
        0 3px 5px rgba(0,0,0,0.25),
        inset 0 1px 0 rgba(255,255,255,0.3),
        inset 0 -5px 5px rgba(100,100,100,0.1),
        inset 0 5px 5px rgba(255,255,255,0.3);
}

It already looks acceptable, but we need to add some pseudo-elements to make it awesome, right? So let’s go.
The first one is meant to improve the embossed look by adding a little recess around the button. So we set it to position absolute, behind the button and we don’t give it any size but we give it some top/right/bottom/left values instead.

And of course, some aesthetics: a soft gradient from top to bottom and some sexy box-shadows.

.switch label:after {
    content: "";
    position: absolute;
    z-index: -1;
    top: -8%; 
    right: -8%; 
    bottom: -8%; 
    left: -8%;
    border-radius: inherit;
    background: #ddd; /* Fallback */
    background: linear-gradient(#ccc, #fff);
    box-shadow: 
        inset 0 2px 1px rgba(0,0,0,0.15),
        0 2px 5px rgba(200,200,200,0.1);
}

And now, the LED, which is the main indicator for the state of the button. Basically, it’s a small circle placed in the middle of the button. A little more fanciness than *just* box-shadows will be used here: a radial gradient. Let’s use a simple one though, just to make the whole thing a little nicer for browsers supporting radial gradients, and make a fallback for others.

.switch label:before {
    content: "";
    position: absolute;
    width: 20%;
    height: 20%; 
    left: 40%;
    top: 40%;
    border-radius: inherit;
    background: #969696; /* Fallback */
    background: radial-gradient(40% 35%, #ccc, #969696 60%);
    box-shadow:
        inset 0 2px 4px 1px rgba(0,0,0,0.3),
        0 1px 0 rgba(255,255,255,1),
        inset 0 1px 0 white;
}

Okay, so now, we have a nice button. Let’s deal with the checked state. When you click on the container, you actually click on the invisible checkbox which sits on top of the label. From here, you have activated the checkbox, meaning you can use the :checked state and a sibling selector. The following is like saying “Hey, the input element is checked, so the following label in the .switch element has to be like this.
So when the button is turned on, we change the background color of the LED (:before pseudo-element), as well as the background of the button. That’s basically it!

.switch input:checked ~ label { /* Button */
	background: #e5e5e5; /* Fallback */
    background: linear-gradient(#dedede, #fdfdfd);
}

.switch input:checked ~ label:before { /* LED */
    background: #25d025; /* Fallback */
    background: radial-gradient(40% 35%, #5aef5a, #25d025 60%);
    box-shadow:
        inset 0 3px 5px 1px rgba(0,0,0,0.1),
        0 1px 0 rgba(255,255,255,0.4),
        0 0 10px 2px rgba(0, 210, 0, 0.5);
}

Example 2

In this example we want to come close to an actual light button switch, so let’s see how far we can take it.

The Markup

The markup is the same as in the first example; a container, a checkbox and a label:

<div class="switch">
	<input type="checkbox">
	<label></label>
</div> 

The CSS

We start by setting a size to our button and hiding the checkbox.

.switch {
    width: 50px;
    height: 100px;
    position: relative;
}

This example requires many more box-shadows to look well. Let’s not forget to add a soft background color and rounded corners, to make the whole thing look even softer:

.switch label {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    background: #cbc7bc;
    border-radius: 5px;
    box-shadow:
        inset 0 1px 0 white,
        0 0 0 1px #999,
        0 0 5px 1px rgba(0,0,0,0.2),
        0 2px 0 rgba(255,255,255,0.6),
        inset 0 10px 1px #e5e5e5,
        inset 0 11px 0 rgba(255,255,255,0.5),
        inset 0 -45px 3px #ddd;
}

And now, let’s add some pseudo-elements. The first one is meant to create the housing, the second one to simulate screws. Let’s start with the housing. It’s pretty much the same thing as in our previous example, plus a little border:

.switch label:after {
    content: "";
    position: absolute;
    z-index: -1;
    top: -20px;
    left: -25px;
    bottom: -20px;
    right: -25px;
    background: #ccc; /* Fallback */
    background: linear-gradient(#ddd, #bbb);
    border-radius: inherit;
    border: 1px solid #bbb;
    box-shadow:
        0 0 5px 1px rgba(0,0,0,0.15),
        0 3px 3px rgba(0,0,0,0.3),
        inset 0 1px 0 rgba(255,255,255,0.5);
}

The second one is more interesting, since we only have one element to create two screws. This is one more job for box-shadows! Because we are using a plain color, we can easily make a shadow look like our main element. We could even make 4, 10 or 42 screw holes with this. But we just need two here:

.switch label:before {
    content: "";
    position: absolute;
    width: 8px;
    height: 8px;
    top: -13px;
    left: 20px;
    background: #666;
    border-radius: 50%;
    box-shadow:
        0 1px 0 white, /* Subtle reflection on the bottom of the hole */
        0 120px 0 #666, /* Faking a second screw hole... */
        0 121px 0 white; /* And its reflection */
}

Now the part of the checked state. In this example only the central part (which is the label) will change when we click it. And we created this material effect only with box-shadows, so easy peasy, let’s just revert them, adapt them, prettify them, and we’re done:

.switch input:checked ~ label { /* Button */
    background: #d2cbc3;
    box-shadow:
        inset 0 1px 0 white,
        0 0 0 1px #999,
        0 0 5px 1px rgba(0,0,0,0.2),
        inset 0 -10px 0 #aaa,
        0 2px 0 rgba(255,255,255,0.1),
        inset 0 45px 3px #e0e0E0,
        0 8px 6px rgba(0,0,0,0.18);
}

Example 3

Let’s get on with something even cooler. This one is adapted from a lovely design by talented Piotr Kwiatkowski. It looks so smooth, doesn’t it?

The Markup

For this switch, we will need an extra element. Let’s pick something quiet and discrete, like an <i>.

<div class="switch">
	<input type="checkbox">
	<label><i></i></label>
</div> 

The CSS

So once again, we hide the checkbox, with the same little snippet of CSS. And, as always, we give a size to the container.

.switch {
    width: 180px;
    height: 50px;
    position: relative;
}

So here we are going to do the following: the label will be the container of the toggle switch. The <i> tag will be the actual toggle element. So the label is only the dark grey background with shadows, nothing more. Pseudo-elements will do the rest.

.switch label {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    background: #a5a39d;
    border-radius: 40px;
    box-shadow:
        inset 0 3px 8px 1px rgba(0,0,0,0.2),
        0 1px 0 rgba(255,255,255,0.5);
}

This is the “frame of the container”, so we give it a nice gradient, and some sweet box-shadows.

.switch label:after {
    content: "";
    position: absolute;
    z-index: -1;
    top: -8px; right: -8px; bottom: -8px; left: -8px;
    border-radius: inherit;
    background: #ccc; /* Fallback */
    background: linear-gradient(#f2f2f2, #ababab);
    box-shadow: 0 0 10px rgba(0,0,0,0.3),
        0 1px 1px rgba(0,0,0,0.25);
}

This pseudo-element is meant to create a soft hollow around the button, so as usual, we’ll use gradient and shadows. A little enhancement for Chrome with the use of a blur filter to make the effect even softer.

.switch label:before {
    content: "";
    position: absolute;
    z-index: -1;
    top: -18px; right: -18px; bottom: -18px; left: -18px;
    border-radius: inherit;
    background: #eee; /* Fallback */
    background: linear-gradient(#e5e7e6, #eee);
    box-shadow: 0 1px 0 rgba(255,255,255,0.5);
    -webkit-filter: blur(1px); /* Smooth trick */
    filter: blur(1px); /* Future-proof */
}

So now, we have a really beautiful frame for our toggle switch. Let’s take care of the switch, shall we?

We turn it into a block element, give it the height of its parent and a little bit more than half of its width. We place it on the left of the container, and give it some nice styles (shadows and gradients, you get it):

.switch label i {
    display: block;
    height: 100%;
    width: 60%;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 2;
    border-radius: inherit;
    background: #b2ac9e; /* Fallback */
    background: linear-gradient(#f7f2f6, #b2ac9e);
    box-shadow:
        inset 0 1px 0 white,
        0 0 8px rgba(0,0,0,0.3),
        0 5px 5px rgba(0,0,0,0.2);
}

This first pseudo-element is used to enhance the aesthetics: it adds a curved effect to the toggle. We place it in the middle, and gave it a gradient from top to bottom, almost the same as the one of its parent but reverted.

.switch label i:after {
    content: "";
    position: absolute;
    left: 15%;
    top: 25%;
    width: 70%;
    height: 50%;
    background: #d2cbc3; /* Fallback */
    background: linear-gradient(#cbc7bc, #d2cbc3);
    border-radius: inherit;
}

The second pseudo-element is the word “ON” or “OFF”, depending on the state of the button. We use the content property to set the right word, and some font styles to make it look like it’s engraved in the background:

.switch label i:before {
    content: "off";
    position: absolute;
    top: 50%;
    right: -50%;
    margin-top: -12px;
    color: #666; /* Fallback */
    color: rgba(0,0,0,0.4);
    font-style: normal;
    font-weight: bold;
    font-family: Helvetica, Arial, sans-serif;
    font-size: 24px;
    text-transform: uppercase;
    text-shadow: 0 1px 0 #bcb8ae, 0 -1px 0 #97958e;
}

So now we have a toggle switch which looks like the one from Piotr, right? Let’s get into the checked state. Three things are changing when we click on the checkbox: the background color, the toggle’s position, and the word (“ON” or “OFF”):

.switch input:checked ~ label { /* Background */
    background: #9abb82;
}

.switch input:checked ~ label i { /* Toggle */
    left: auto;
    right: -1%;
}

.switch input:checked ~ label i:before { /* On/off */
    content: "on";
    right: 115%;
    color: #82a06a;
    text-shadow: 0 1px 0 #afcb9b, 0 -1px 0 #6b8659;
}

Example 4

Our last example is based on another work from Piotr Kwiatkowski, but slightly edited by myself in order to have the same color theme as the others.

Let’s do something really amazing on this one, with an icon font and transitions!

The Markup

The markup will be pretty much the same as in the third example. The class on the <i> tag is required in order to make FontAwesome work.

<div class="switch">
    <input type="checkbox">
    <label><i class="icon-off"></i></label>
</div> 

The CSS

I won’t cover here all the FontAwesome stuff because I’m pretty sure it has already been covered in another article, plus probably all of you have already done that before. Just make sure that

  • your @font-face stuff indicate the correct paths to the files,
  • one of your CSS files includes FontAwesome related styles.

So let’s start with the basics: checkbox hiding and container sizing:

.switch {
    width: 150px;
    height: 150px;
    position: relative;
}

Now, let’s take care of the label which is basically our big old button. Note that we include a few font style here. Since all of them are inherited, they will go straight to the <i> element (icon). So nothing new, a sweet gradient and some box-shadows for a beautiful effect:

.switch label {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    color: #a5a39d;
    font-size: 70px;
    text-align: center;
    line-height: 115px;
    text-shadow: 0 2px 1px rgba(0,0,0,0.25);
    border-radius: 50%;
    background: #b25244; /* Fallback */
    background: linear-gradient(#f7f2f6, #b2ac9e);
    transition: all 0.3s ease-out;
    z-index: -1;
    box-shadow:
        inset 0 2px 3px rgba(255,255,255,0.13),
        0 5px 8px rgba(0,0,0,0.3),
        0 10px 10px 4px rgba(0,0,0,0.3);
}

Now, both label pseudo-elements are used to make the material effect around the button. We apply the blur filter to the closest one to make it a bit more subtle.

.switch label:before {
    content: ""; 
    position: absolute;
    left: -10px;
    right: -10px;
    top: -10px;
    bottom: -10px;
    z-index: -1;
    border-radius: inherit;
    box-shadow: inset 0 10px 10px rgba(0,0,0,0.13); 
    -webkit-filter:blur(1px); /* Smooth trick */
    filter: blur(1px); /* Future-proof */
}

.switch label:after {
    content: ""; 
    position: absolute;
    left: -20px;
    right: -20px;
    top: -20px;
    bottom: -20px;
    z-index: -2;
    border-radius: inherit;
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.1),
        0 1px 2px rgba(0,0,0,0.3),
        0 0 10px rgba(0,0,0,0.15);
}

Okay, let’s deal with our extra element: <i>. FontAwesome uses the :before pseudo-element to show the icon, so we can’t use it. But, we can use the :after one, and we will because we need something to make the upper part of our button.
As usual, we give it a nice gradient, some subtle shadows, and a 1px blur to make it smoother.

.switch .icon-off:after {
    content: "";
    display: block;
    position: absolute;
    width: 70%;
    height: 70%;
    left: 50%;
    top: 50%;
    z-index: -1;
    margin: -35% 0 0 -35%;
    border-radius: 50%;
    background: #d2cbc3; /* Fallback */
    background: linear-gradient(#cbc7bc, #d2cbc3);
    box-shadow:
        0 -2px 5px rgba(255,255,255,0.05),
        0 2px 5px rgba(255,255,255,0.1);
    -webkit-filter:blur(1px); /* Smooth trick */
    filter: blur(1px); /* Future-proof */
}

And now, the checked state; pretty simple: the icon turns green, and the button looks pressed:

.switch input:checked ~ label { /* Button */
    color: #9abb82;
    box-shadow:
        inset 0 2px 3px rgba(255,255,255,0.13),
        0 5px 8px rgba(0,0,0,0.35),
        0 3px 10px 4px rgba(0,0,0,0.2);
} 

Final words

So finally, what do we need to make beautiful buttons with pure CSS? Mostly shadows! But subtle gradients and rounded corners help a lot, too. To turn a simple button into a button switch, we just need one more thing: a checkbox.

Please, keep in mind that this kind of stuff is pretty tricky, so not every browser will display it correctly. But, you can make the whole thing fall back to a simple checkbox very easily by using a filtering selector, for example:

   .switch:not(:checked) label {
        /* Styles for label go here... */
    }

    .switch:not(:checked) input {
        /* Styles for input go here... */
    }

In this case, :not(:checked) is a filter so that browsers that don’t support :checked don’t follow these rules. Check out this article by Lea Verou to find out more about this technique: Accessible star rating widget with pure CSS

Anyway, with a little more time, we can make beautiful things like these:

Thank you for reading this tutorial, and if you have any question or related work to show, please do. πŸ™‚

Tagged with:

Kitty Giraudel

Non-binary accessibility & diversity advocate, frontend developer, author. Real life cat. They/she.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 46

Comments are closed.
  1. OMG You read my mind! I was just looking into these kinds and styles of check boxes the other day and I wished there was a tutorial on them here on Codrops, coz the quality of tutorials here exceeds that of any other site I know. Thank you thank you thank you for this!
    awesome tut! πŸ™‚

  2. @Hugo I have one question hope u get to answer it. Example #3
    How can I make the “slider” SLIDE from one side to another when someone checks the boxes? I”ve seen this slide smoothly on a website once and I’ve been wondering how it can be done. Can u help me with that ? Should I add a transition property somewhere?

    • I made a JSFiddle to show you how to do it : http://jsfiddle.net/7Lh7n/. Instructions are in the JavaScript box.

      So basically, all you have to do is adding a CSS transition to the toggle element, and change the left value to a right value, or the right value on the checked state to a left value. You can’t go from left to right or right to left, only left to left, or right to right (have a closer look to the JSFiddle if you didn’t understand).

      There is a problem though : text is moving as well. So you see “ON” or “OFF” sliding over the whole thing, which is pretty ugly, that’s why I didn’t make the toggle slide in the demo.

      You could fix it by making the text appear after the sliding, but only Firefox is able to do this. When other browsers will support transitions and animations on pseudo-elements, we will be able to make waaaaay more things. πŸ˜‰

  3. Davide De Maestri is right. Perhaps put a disclaimer at the part where you explain about the checkbox hack telling that it’s pure experimental and that we actually should use JavaScript for behavior and not a form element.

    • The :checked pseudo class is intended for such purpose and is not experimental. It works everywhere, even when JavaScript is turned off.

    • Seriously? Just an empty onclick=””? Dafuq is this glitch!

      Thanks a lot for sharing Ryan. πŸ™‚

  4. @Hugo Thanks a lot for taking the time to reply and prepare the jsFiddle demo, I really really appreciate it :))
    Great job on this tutorial, again, and have a great time!
    Will check the jsFiddle demo now and study it πŸ™‚ Can’t wait for css3 to be fully supported by all browsers!

    • @Hugo, am using Firefox, but the demo on jsFiddle doesn’t “slide”, it just changes state like it does here.. no sliding :/ any idea why?

    • @Hugo Yeah I did πŸ˜€ still didn’t work on jsFiddle (no idea why), so I pasted the code and created a new page on my desktop and it worked ! πŸ™‚ Thank you!! I appreciate your continuous replies πŸ™‚ Thanks again πŸ™‚
      Will try to tweak it a little bit and see what I can do about the sliding “ON/OFF” just like u suggested πŸ™‚

    • Your testcase works because your checkbox is on top of the label. It means you do not use the label at all. If you want to make a test, open the Web Inspector, and delete the label. You’ll see it still works. πŸ˜‰

      However, I did some tests on an iPad (I don’t have any iPhone and I never had, so I’m not very familiar with Mobile Safari). I tried this pen on CodePen : http://codepen.io/HugoGiraudel/pen/EurGj. In this case, the checkbox is out of the screen, and the label triggers the event. It works on iPad 2.

      And this is weird, because Pedro Botelho (and Mary Lou probably agrees) told me the normal behaviour (for attribute on the label, id attribute on the checkbox) doesn’t work on Mobile Safari.
      Quoting :
      “we also don’t use labels any more because normally, labels are used to trigger :checked but that does not work in mobile Safari (i.e. iPad/iPhone)”

      So unless it works in a different way on iPhone, the issue with the normal behaviour of a label with a for attribute has been fixed. I guess both ways can be used then.

      Anyway, hidding a checkbox and checking it by clicking on a label is using the checkbox hack. It doesn’t matter if you put the checkbox out of the screen or if you hide it on top of a graphical element, the result is the same. πŸ˜‰

    • I remember that we’ve had some issues with that, for example: http://tympanus.net/Tutorials/CSS3FilterFunctionality/ This does not seem to work… more about that: iOS: input labels don’t focus on associated fields. The empty ‘onclick’ is also suggested as a workaround, just like Ryan mentioned. I kind of like the opacity:0 trick because you can use other elements besides the label; I’m just really curious if there is a solution for FF on Linux for that πŸ™‚ Wish I could test it somehow…

  5. Well, that may be nice eye candy, but whats about user experience and usability?
    First: All that examples only work nice with huge Images. Try to find clean UI solutions, that are not bigger than a standard form element and as clean as the native ones. You will hardly find one!
    Second: Color-only-states absolutely fail in usability effort. Ever thought about color blindness? Low-contrast exaples like example #2 are even worst.

    • This tutorial was not about usability. You noticed it very well: all those examples are pretty big.

      The point was to show how to create a switch button with CSS only, no JavaScript. Once people understand the way it works, we can dig into more useful use cases.

      Based on this technique, you can use customized form elements like this one: http://codepen.io/simurai/pen/rJwpz (from Simurai).

  6. Doesnt work on firefox 15.0.1

    The input element is small the height:100% and width:100% doesnt work
    so it is hard to click on right place t switch button

    Work well on chrome

  7. yes, I am also having the same problem and I am using Firefox on Linux, version 15.0.1

    • Yup. This is an issue I discovered while making this tutorial. We were a few to try some things to fix it, without success. If you happen to find a solution, please tell us. πŸ™‚

    • I fixed that on linux by addind an id to the checkbox and a for attribute to the label (corresponding to the checkbox).

      Note: you may have to change the cursor for thelabel (instead of the input)

  8. I was wondering, could show us how to get this to center in a table with a few rows and two columns, I am having some trouble getting it to line up right, I would really appreciated it.

  9. I’m on an iPhone 4S and there seems to be latency when pressing the button. Shouldn’t it be immediate I you are using all CSS?

  10. I tried to implement this on blogger bit it didn’t work out. It only appears 1 button and nothing more. No effects. Any tips and tricks? And by the way, the buttons are pretty cool.

  11. Hey, this is a good one. I can change the size of the customized checkboxes just with changing the height and width of the switch without changing anything more πŸ˜€
    Thanks …

  12. Great examples! however, This doesn’t appear to work on the default android browser. changing the opacity of the checkbox you can see the box is getting checked but the css changes don’t seem to get triggered. Any thoughts and how to get this to work on the android default browser? (BTW it works in chrome on android)

    • Found my own answer. Turns out Android default browser doesn’t like the ~ operator in css (siblings). Instead use the + operator in the selector which means adjacent to. This works for all the demos on this page and should work with all the browsers it currently works with now as well as Android’s default browser.

  13. this is needed for demo3 to work on firefox (tested on 18)
    .switch input[type="checkbox"] {-moz-appearance: none;}

  14. I’m a novice and I found that input must be in front of label,or it don’t work