No Time Dad

A blog about web development written by a busy dad

Easy Toggle Switch with HTML and CSS

Intro

Toggle switches are underrated in my opinion. I think that they provide a nicer user experience than clicking “yes” or “no” radio buttons and they can add some flair to a page. You can find lots of different versions of them on the internet, but a lot of them seemed overly complicated or required a separate library. To me, that seems sort of silly for such a tiny element but maybe I have under estimated the humble toggle switch.

Below is what we will be building. Seems simple enough, right?

Getting Started

The slider is going to consist of a label element with a two child elements; a hidden input of type checkbox, and a span that will be the body of our slider. We’ll use a ::before pseudo-element on the span element to create the circle “button” that is common for most sliders you’ll see. For some extra flashiness we’ll add a transition when the toggle is clicked so it slowly moves from the side to side.

Structuring the Toggle Switch

We’ll start with our label element and add the two child elements with their respective classes. For now, we’ll leave off the ::before pseudo-element so it is easier to see how the slider behaves.

Basically, what we have is the container for the slider that will change colors when we click on it. We’ll use the :checked pseudo-class to toggle the background-color value from lightgray to cornflowerblue. There isn’t any need for JavaScript to create this toggle switch, css gives us everything we need.

The Switch__Input selector will hide the checkbox itself, but still allow us to keep track of whether the input is checked or not. You might notice that the selector Switch__Input:checked + .Slider shown below in the css contains a + sign. This is simply a way to target the element directly after a given element. Essentially it is targeting what would be called the “adjacent sibling” element. You can read more about in the MDN docs. Keep in mind that these kind of combinators could be a little brittle if you move element around or add elements. For our simple use case here it should be fine though.

Clicking on the example below should toggle the color from blue to gray.

<label class="Switch">
  <input type="checkbox" class="Switch__Input" checked>
  <span class="Slider"></span>
</label>
.Switch {
  position: relative;
  /* Set the size of our the slider container*/
  width: 60px;
  height: 34px;
}

.Slider {
  position: absolute;
  /* Change the cursor so the user knows they can click on the element */
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  /* The border-radius could be any value since we have specified the height and width 
  but removing it would cause the container to have square corners */
  border-radius: 1rem;
  /* Default the background-color to lightgray unless the checkbox is in a checked state */
  background-color: lightgray;
}

.Switch__Input {
  /* Hide the checkbox input from the user but keep it in the DOM */
  opacity: 0;
  width: 0;
  height: 0;
}
.Switch__Input:checked + .Slider {
  /* Change the color of the slider container when it is clicked (or checked) */
  background-color: cornflowerblue;
}
Adding the Circle and Transition

Now that we have the container built for our slider, we can add our circle ::before pseudo-element. We can also add our transition effect that will move the circle from right to left when it is clicked. The property that does the actual movement is transform: translateX(n);, which will move the pseudo-element along the x-axis a given number of pixels. In our case, the given number of pixels will be the same as the circle width.

As for the transition, we’ll use a generic .3s transition to move the circle to either side of the slider. If you want it to move slower or faster, you simply adjust the transition value. The higher the value the slower the transition and vice versa.

We’ll be adding a new selector for the ::before pseudo-element, as well as adding the transition property to the existing Slider selector. To handle the left to right change we’ll add the translateX mentioned above to the new pseudo-element.

Below is our updated css. The html will remain the same

<label class="Switch">
  <input type="checkbox" class="Switch__Input" checked>
  <span class="Slider"></span>
</label>
.Switch {
  position: relative;
  width: 60px;
  height: 34px;
}

.Switch__Input { 
  opacity: 0;
  width: 0;
  height: 0;
}

.Slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 1rem;
  background-color: lightgray;
  /* New property */
  transition: .3s;
}

/* New selector for the circle pseudo-element */
.Slider::before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  border-radius: 50%;
  background-color: white;
  transition: .3s;
}

.Switch__Input:checked + .Slider::before {
  /* Move the circle pseudo-element from left to right and right to left when the input is checked */
  transform: translateX(26px);
}

.Switch__Input:checked + .Slider {
  background-color: cornflowerblue;
}

Thoughts and Final Code

That is pretty much it. Not bad, right? Well, I think there are still some things to consider. The biggest being accessibility, or lack there of. It really needs a way to implement :focus, which I couldn’t find a good solution for or a solution that worked how I expected it to.

The other thing to think about is that you might want this toggle switch to do something other than move left and right. You might want to track the checked value and use it to show or hide elements, and for that you’d definitely need some JavaScript. Anyways, this is a good starting point for a really nice toggle switch I think.

<label class="Switch">
  <input type="checkbox" class="Switch__Input" checked>
  <span class="Slider"></span>
</label>
.Switch {
  position: relative;
  width: 60px;
  height: 34px;
}

.Switch__Input { 
  opacity: 0;
  width: 0;
  height: 0;
}

.Slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 1rem;
  background-color: lightgray;
  transition: .3s;
}

.Slider::before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  border-radius: 50%;
  background-color: white;
  transition: .3s;
}

.Switch__Input:checked + .Slider::before {
  transform: translateX(26px);
}

.Switch__Input:checked + .Slider {
  background-color: cornflowerblue;
}