What I Learned at Work this Week: A CSS Toggle

Mike Diaz
10 min readAug 8, 2021

Last week, I was assigned a unique ticket for a high priority client. The client uses HTML and CSS files that my company generates to add a checkbox element to their checkout if shoppers want to opt in to our services. What was unique about the ticket was that the client wanted us to customize the code to match their desired style, which included turning a standard HTML checkbox into a round toggle with a check mark inside the circle when it’s active:

The good news here is that because this was such an important client, I was given help by an Engineering Manager who ended up doing most of the tricky CSS after I had built the slider. But that also means that there’s a lot of this code that I don’t fully understand yet. And that means it’s time to blog!

Checkbox to Slider

I was able to accomplish the first part of the ticket by searching online, as CSS toggles appear to be pretty common requests. I started with some relatively robust HTML and CSS, but most of it was used to format and space text. For our example, we’ll isolate the checkbox and start with no CSS:

<div class=”checkbox-wrapper”>
<div class=”checkbox__input”>
<input class=”input-checkbox” type=”checkbox” value=”1" name=”placeholder”>
</div>
</div>

To turn this into a toggle, I followed the instructions from this W3 Schools post. First, we have to add a label around our input and a span inside that label:

<div class=”checkbox-wrapper”>
<div class=”checkbox__input”>
<label class=”switch”>
<input class=”input-checkbox” type=”checkbox” value=”1" name=”placeholder”>
<span class=”slider”></span>
</label>
</div>
</div>

If you want to see all the CSS in one place, click the W3 link. I’m going to break down the different sections to see why they’re needed. To start, we want to hide the default checkbox:

.switch input {
opacity: 0;
width: 0;
height: 0;
}

Setting opacity to 0 will make an element invisible, and reducing the width and height will give us the assurance that our element isn’t causing any positioning issues. With that out of the way, we can define a space for our slider:

.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}

We set our position to relative so that it is responsive to other elements around it. The inline-block display provides the element with the inline property of appearing next to other elements (like it’s on the same line as them) with the block property of having its own space. It’s the latter which allows us to set a width and height. Next we start to define styles for our span:

.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}

We can make the position absolute because it’s contained inside of the switch label, which is relative and therefore won’t overlap other elements. The cursor: pointer property changes the cursor to a hand when hovering over our element, indicating to the user that they can interact with it by clicking. Top, left, right, and bottom once again set position within the parent element, so we’re just making sure our slider isn’t inheriting any positional oddities. The background color can be whatever we choose and W3 in this case chose #ccc, a light grey.

Our two transition properties are redundant, but that may be because -webkit-transition is deprecated and therefore not supported by every browser. Transition can be broken up into four categories: property, duration, timing-function, and delay. This example uses the shorthand, so we could have added all four as separate arguments to this single property if we wanted to. Since we only provided the value .4s, our browser will understand that this represents duration and will make our transition last 0.4 seconds. It stands to reason that the transition in question is between the two positions of our slider.

.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}

:before, or ::before, creates a pseudo-element that appears before the element selected. Traditionally we think of CSS as editing elements already in our HTML, but a pseudo-element is like creating new HTML that we can then style. In this case, that element has a height and width of 26 pixels, is positioned 4 pixels to the left and 4 up from the bottom, and has a white background. It has no content (ie HTML properties like a classname) and transitions at the same rate as our slider element. Have you guessed what it is?

It’s our toggle indicator! We used :before to create this element from scratch and style it to be a white square. But our toggle doesn’t move yet, so let’s take a look at the next step:

input:checked + .slider {
background-color: #2196F3;
}

We’ve got a new type of selector here, which uses the :checked pseudo-selector. This means that the element will only qualify for this style if it is considered “checked,” which is a property shared by radio, option, and checkbox elements. Since our span shares a label with our hidden input, clicking on it will switch the status between checked and unchecked.

Remember that the slider span is currently our grey rectangle. If you look up the hexadecimal color in this CSS (I used ColorHexa), you’ll see that it’s a medium blue. So when we toggle, we should now see the color change between grey and blue. That transition takes exactly 0.4 seconds. If you want to confirm that, change the transition property under .slider from .4s to 4s and see the difference.

Next we have a minor update to an element when it’s in focus:

input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}

The arguments here represent the shadow’s position (0 and 0 being x and y axis, making the shadow appear directly behind the element) and the blur radius (1px meaning an extremely sharp shadow). The fourth argument is the color, which is the same as our background color. It’s really hard for us to see but it gives our slider a bit of depth. We’re almost done with our basic slider, we just have to make the white square move back and forth:

input:checked + .slider:before {
-webkit-transform: translateX(26px)
-ms-transform: translateX(26px);
transform: translateX(26px);
}

So now we’re specifically selecting the slider:before pseudo-element when its parent input is checked. In that specific case, we’re going to use translateX to move it 26 pixels along the X axis, to the right. There are three properties here, but they’re redundant. If we want to stick to one we can just use transform. The transition, as we know, takes 0.4 seconds and once the pseudo-element no longer matches this CSS selector, it will be reverted to its original position, also taking 0.4 seconds to move back. Try it out!

Customizing the slider

In most cases, a client isn’t going to want to have the classic W3 slider on their website. In my case, I had to make a few changes:

Round Toggle

Luckily for me, the W3 example also provides a round toggle option. We’re just adding a border-radius property to our slider and our slider:before pseudo-element:

.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}

Border-radius introduces a curve into angled shapes. As we can see, it can be conveyed in a static length or in a percentage. They both refer the the size of a circle or an ellipse’s diameter, so I’d say the important takeaway is the larger the number, the more dramatic the curve. When I add these selectors to my CSS and add the round class name to my slider element, the shape changes:

Black Background

Even before I had analyzed the CSS, I could figure this out without much trouble by looking for any defined color properties and changing them to see what happened. In this case, we remember exactly where we set our background color to blue. I’ll also change our shadow color even though it’s hard to see:

input:checked + .slider {
background-color: #000000;
}
input:focus + .slider {
box-shadow: 0 0 1px #000000;
}

Checkmark inside toggle when checked

This was the step where I had to call in some help (and was very grateful to get it from an Engineering Manager). The first thing I had to do was figure out how to render a checkmark that looked like the one the client showed in their sample. I wasn’t sure if it was an image that they were hosting, if it was a version of the checkmark character (√, or option + v on a mac), or if it was pure CSS. But after the client pointed out that there was another example of the checkmark on their site already, I got the advice to inspect that element to find my answer. It turns out it was using SVG:

background: no-repeat 50% url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' viewBox='8.9 0.3 10.3 8'%3E%3Cpath d='M12.6 8.1L8.9 4.3l1–1.1 2.7 2.7L18.1.5l1 1z'/%3E%3C/svg%3E")

SVG stands for Scalable Vector Graphics. The idea here is that rather than using pixels, SVG images can be programmed with curves at the lowest level, meaning quality will not be degraded no matter how large or small we make them. SVG images can be designed using third party software and hosted in our program, but we can also code them out right on the page as shown in this example. If we look closely at the string being passed to url(), it describes the type of data (an SVG image), standards of SVG notation like charset and xmlns, the size of our image, and then the code for the image itself, which includes viewBox and path.

Fortunately I didn’t have to change anything about the checkmark because the SVG image code is nearly impossible to read (for me at least). I could just place this background property into the slider pseudo-element I had already rendered:

.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
background: no-repeat 50% url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' viewBox='8.9 0.3 10.3 8'%3E%3Cpath d='M12.6 8.1L8.9 4.3l1–1.1 2.7 2.7L18.1.5l1 1z'/%3E%3C/svg%3E")
}

For those following along at home, you might get concerned when you apply this CSS. The result isn’t exactly what we’re looking for:

We’ve got the checkmark, but instead of fitting into the white circle, it’s replaced it! Our background-color property is still set to white, but by adding a new background property, we’ve overwritten that. CSS styles are determined in order from top-to-bottom — if a property is defined twice, the lowest one will take priority. To fix this, we can move background-color: white to the bottom of our style object:

.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
-webkit-transition: .4s;
transition: .4s;
background: no-repeat 50% url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' viewBox='8.9 0.3 10.3 8'%3E%3Cpath d='M12.6 8.1L8.9 4.3l1–1.1 2.7 2.7L18.1.5l1 1z'/%3E%3C/svg%3E")
background-color: white;
}

Since our checkmark is part of the previously existing pseudo-element, it carries all of its other styles, including its transition details. That is to say that the checkmark moves when the white bubble moves. Awesome! But we only want the check to appear when the toggle is checked. Oh wait — we have a specific selector for that situation! Let me move the SVG background to that element:

input:checked + .slider:before {
background: no-repeat 50% url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' viewBox='8.9 0.3 10.3 8'%3E%3Cpath d='M12.6 8.1L8.9 4.3l1–1.1 2.7 2.7L18.1.5l1 1z'/%3E%3C/svg%3E")
transform: translateX(26px);
background-color: white;
}

This time we knew that we’d have to add the background-color or else the bubble would disappear. Now we see the checkmark only appear when the bubble is on the right, which is exactly what we want. The one strange thing is that it isn’t transitioning vertically, but seems to have some horizontal movement:

Remember that the background properties of our SVG checkmark include no-repeat and 50%. The second argument, 50%, is what renders it directly in the middle of the bubble. But that property doesn’t exist in the unchecked version of the bubble (.slider:before). So what we’re seeing is a transition from the default position (the upper-right corner of the element) to the middle. We can fix that, and complete our slider, by adding background-position: 50% to .slider:before:

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

We made it

When I was asked to add the checkmark to this slider, it felt like an impossible task. I was fortunate to have support because after seeing the completed code, it seems much more straightforward than I realized. It’s great to be able to work backward from a solution because it helps us to build a variation in the future. I’ve got a new request for a custom checkbox that I have to work on tomorrow and now, I’m more confident than ever that I’ll be able to make it work.

Sources

--

--