loading...

Making A Switch In CSS

jckuhl profile image Jonathan Kuhl ・7 min read

Let's switch it up!

For a recent project I thought I'd use Materialize CSS and after working with it, determined that, while it's a good library, it wasn't for my project. However, I really liked the switch element they designed so I decided to remake it myself in CSS.

Doing so required I go into areas in CSS I'd never gone before, namely, pseudoelements. By manipulating ::before and ::after, I can make a functional switch out of a checkbox.

Below is the final product. It's a switch that toggles from off to on, and turns grey and unusable when disabled.

Getting Started

We're going to begin with the humble checkbox and wrap it in a label, as we should so that when the user clicks on the label, it will toggle the checkbox

For accessibility purposes you should always use labels in conjunction with form elements.

To ensure our CSS doesn't affect other checkboxes on our site, we'll give the label a class, and modify that class and its descendents as we continue.

<label class="switch">
  Off
  <input type="checkbox">
  On
</label>

Let's set up the CSS:

.switch input[type="checkbox"] {

}

The first thing we need to do is get rid of the default checkbox styling. This took some research, but I found the solution, -webkit-appearance: none;. Our checkbox will now disappear.

Then we need to determine the placement of our knob (the red or blue circle) and the little slider the knob "travels" on (the grey rectangle in the back). These will be made with pseudo elements and they need to be moved relative to the position of the checkbox element.

That's a big hint, we will need to style the knob for absolute positioning, but absolute positioning, as a default will be based off the whole window. We'll want position: relative on our checkbox because it is the parent element for the ::before and ::after pseudo elements.

Finally, we'll want some spacing around our switch, once it appears, so the "on" and "off" text doesn't smash right next to it, and we can do this with margin.

.switch input[type="checkbox"] {
  -webkit-appearance: none;
  position: relative;
  margin: 0 1rem;
}

That sets up our checkbox. But right now, we have nothing to see, so lets make the slider

Mmmm Sliders

Nothing would please me more than a plate of mini burgers. But this a CSS article, not a local small town grill with American food, country music and goofy sh*t on the walls.

Shenanigans

In this context, the slider is just the track our knob travels on. We'll make it out of the ::after pseudo element.

But first, what is the ::after element, and for that matter, the ::before element? Well, pseudo elements are elements that come with various tags that can handle various things. I know that's a very generic and unhelpful response, but there are a lot of them. Some, like ::first-line can modify the first line of a paragraph. There's an experimental one that lets you mess around with ::spell-check. The MDN has a lot of information on pseudo elements

::before and ::after are elements found on most HTML elements and they are considered children of their element. ::before is the first child and ::after is the last child. For our purposes, it won't matter which one is the first or last child. These can be used for a lot of purposes, even creating mustaches.

But we're going to keep it simple and use ::after to create our slider. It'll just be a regular rectangular grey box.

To target it, we simply need to append ::after to a new css rule

.switch input[type="checkbox"]::after {

}

There are five things we need to do to make it appear. First, pseudo elements need some content. Ours will be a simple empty string, so content: '' will suffice. Next, it needs to have display property. We'll make it block, because after some finagling, that's what I found works best here. And it needs a background color, and I picked grey.

Don't make the mistake I always do of setting the color property! color is for text! background is what we want to set!

Finally, we need height and width. As it should be set within a body of text, having it based on the root em (rem) seems the most logical. I set it to be 1 rem wide and 0.5 rem high.

.switch input[type="checkbox"]::after {
  content: '';
  display: block;
  width: 1rem;
  height: 0.5rem;
  background-color: grey;
}

A wild grey rectangle appears!

Styling the Knob

Now for the knobs. Here we have a knob that can be either right or left, but is always a circle, always has a shadow (to make it have some 3d effect) and always has the same content and size, so we want to write our CSS to be DRY, Don't Repeat Yourself.

We want to write a baseline for our ::before pseudo element.

.switch input[type="checkbox"]::before {

}

Again, we need to give it content, dimensions and a display property for it to show.

.switch input[type="checkbox"]::before {
  content: '';
  display: block;
  width: 1rem;
  height: 1rem;
}

That will make a square for us. We can't see it yet because there's no color though, so we'll want to make the rules for when the checkbox is checked and unchecked, since the color changes depending on that value.

Targeting a checked checkbox is simple, we can make a new rule that selects :checked. Targeting an unchecked box is a little more tricky because there's no :unchecked. We have to use the :not() pseudo selector, so we can make two new rules:

.switch input[type="checkbox"]:checked::before {
}

.switch input[type="checkbox"]:not(:checked)::before {
}

The order is important here. We want to target the ::before pseudo-element of a checked input element of type checkbox that is descendent of an element of the switch class. We don't want :checked after ::before because ::before doesn't have a checked attribute.

If it is checked, I want it to be blue and moved to the left 10 pixels. And if it is not checked, I want it to be red and moved to the right 10 pixels.

.switch input[type="checkbox"]:checked::before {
  left: 10px;
  background-color: blue;
}

.switch input[type="checkbox"]:not(:checked)::before {
  right: 10px;
  background-color: red;
}

Because we're modifying left and right attributes, our baseline needs to be set for position: absolute:

.switch input[type="checkbox"]::before {
  content: '';
  display: block;
  position: absolute;
  width: 1rem;
  height: 1rem;
}

Now we're almost there. The knob is a bit uncentered on the track, to fix it, I found that setting the top position -0.25 put it where I wanted it:

.switch input[type="checkbox"]::before {
  content: '';
  display: block;
  position: absolute;
  top: -0.25rem;
  width: 1rem;
  height: 1rem;
}

And finally, let's make it a circle with some shadow. Circles are easy, just give it a border radius of 50%. Shadows on the other hand, I never hand craft shadows, I found a good online tool for that. I like to use CSSMatic for this. They have a real-time box shadow tool that autogenerates the code for you. With the border radius and values I liked, the final CSS for the baseline selector looks like this:

.switch input[type="checkbox"]::before {
  content: '';
  display: block;
  position: absolute;
  top: -0.25rem;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
  -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
  box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}

And there you have it! A functional switch!

But wait, there's more!

No form element is complete without styles to target :disabled. Our users need a visual cue to know this element can't be used at the moment. For this, we can take our knob, our ::before element, and change the color if the checkbox is disabled:

.switch input[type="checkbox"]:disabled::before {
  background-color: darkgrey;
}

That will turn the knob grey if it is disabled. Try adding 'disabled' to the input tag to test it out.

And we're done here!

Here's the full code:

<label class="switch">
  Off
  <input type="checkbox">
  On
</label>
.switch input[type="checkbox"]:checked::before {
  left: 10px;
  background-color: blue;
}

.switch input[type="checkbox"]:not(:checked)::before {
  right: 10px;
  background-color: red;
}

.switch input[type="checkbox"]::before {
  content: '';
  display: block;
  position: absolute;
  top: -0.25rem;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
  -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
  box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}

.switch input[type="checkbox"]::after {
  content: '';
  display: block;
  width: 1rem;
  height: 0.5rem;
  background-color: grey;
}

.switch input[type="checkbox"] {
  -webkit-appearance: none;
  position: relative;
  margin: 0 1rem;
}

.switch input[type="checkbox"]:disabled::before {
  background-color: darkgrey;
}

This is a simple little switch that can add a little flavor to a site. It works great for binary situations, if the user wants this or the user wants that.

Feel free to use and modify this as you see fit. Try adjusting size or colors. Make it fancier, have fun with it. And if you see any way this can be done better in CSS, let me know.

Happy coding

Posted on Mar 30 '19 by:

jckuhl profile

Jonathan Kuhl

@jckuhl

I am a software development engineer in test for Infosys. My job is officially to write automated tests in Selenium Webdriver. I'm also a web developer as a hobbyest

Discussion

markdown guide
 

Nice job! Tip: instead of using the left and right properties for moving the knob, you could also use transform: translateX() which will give you better performance (especially if animating things), since it does not require the browser to repaint and re-layout anything, it only updates in the compositor phase.
Here's my version of your switch: codepen.io/awolfika/pen/yryeGb