How to Create Animated Toggle Switches with Just CSS and Implement a Dark Mode Feature
David Herbert💻🚀
Posted on October 17, 2021
A toggle switch as we know it allows us to choose between two opposite states such as an on/off state, whether it's turning on a light bulb or switching off a microwave, toggle switches are everywhere because we use them daily. It's no surprise then that digital toggle switches when implemented on a website, gives it a more modern feel and facilitates micro-interactions between users and the site. Take Hashnode for instance, it has an animated toggle that allows us to change the theme of the entire site from a light mode to a dark mode and vice versa. 👇
You might have seen this or even fancier animated toggles on other websites as well and wondered just how these toggle switches are made and how these website themes are changed on the go because, as a developer/designer you've got to admit... It's a pretty cool modern feature to have on your own website and projects. That's what we are going to be learning in this article, by the end of this article, you'd have not only learnt how but also mastered the art of making toggle switches and implementing light mode/dark mode.
Prerequisite
- You only need to have basic HTML & CSS knowledge i.e I shouldn't have to explain what input, label, width, height etc... are.
- A text editor and browser of course if that wasn't obvious already.
- You enjoy explanations geared towards 5-Year-olds :)
Understanding CSS Just a Little Better
The idea of creating a toggle switch with just CSS can seem really daunting at first when you think about it, you'd normally assume you needed a library or at the very least JavaScript for such a task, and although you aren't exactly wrong as most people use those for creating theirs, this is a somewhat trivia task that CSS can handle. Before diving into our code I'd like to shed some light on two particular features in CSS that some of us overlook, but because our code is dependent on these features we need to talk about them for a second.
The first has to do with the input and label elements in forms. We won't be creating any forms here but for illustrative purposes, I'm going to create a label and an input element with the type set to checkbox in our HTML document.
<label>A confused label</label>
<input type="checkbox"/>
Output👇
Semantically, we have created a label
with an input
element next to it, by just looking at it in the browser you can easily tell that this label
is meant for our checkbox and you could even style both to look related. There's nothing wrong with that since clicking on the checkbox toggles its state to checked/unchecked as intended. But to that label
itself, it has no idea what it is doing in that document or that it is supposed to be associated to that checkbox input
, by appearance alone they might look related but programmatically they are unrelated, so clicking on this label does absolutely nothing. But CSS has an attribute that you can assign to a label
to tell it what relationship it has with an input
element and that is the for
attribute and for this attribute to link a label
to an input
, the input
would have to have an id
with the same value as the value assigned to the for
attribute in the label
. So let's modify our code so you can see the effect of this link.
<label for='switch'>A Linked label</label>
<input id='switch' type="checkbox"/>
Notice that after setting the value of the for
attribute in the label
and id
in the input to the same value switch
, when either the checkbox or label is now clicked the checkbox gets toggled as they now have a relationship established (they are linked).
The second feature has to do with pseudo-classes. Now because most people confuse pseudo-classes and pseudo-elements as the same thing we need to establish the distinction between these before continuing.
Pseudo-Element: A pseudo-element is simply a selector used to insert/inject an artificial (not present in the HTML markup) content on the webpage and to style specified parts of an element. A few examples are the ::before
, ::after
, ::placeholder
, ::first-letter
etc... Note that they are declared with two colons i.e. ::
but since we have no interest in them, I'll explain no further.
Pseudo-Class: A pseudo-class on the other hand is simply a selector that targets the state of an element and does a tiny bit more. A typical example of this is the :hover
state of an element which means you want to apply certain CSS rules when the user hovers over the element with their mouse. The interesting part is certain elements have their own unique pseudo-classes, an a
link for instance has a:visited
state, Google uses this on their search results where if you click on a link to visit a site and then come back to the search results, the color of the link you visited would change from blue to purple. And just like a link, a checkbox also has its own unique pseudo-class of :checked
and :unchecked
. Note that the pseudo-class is declared with a single colon sign i.e. :
.
That brings us to the most interesting part of these pseudo-classes, we can actually target and style other elements based on the state of a particular element using pseudo-classes, Let's go back to our label example and modify the order in which they appear so the input appears before the label.
<input id='switch' type="checkbox"/>
<label for='switch'>A Linked label</label>
Now let's change the color of this label whenever the checkbox is toggled (checked) and then I'll explain further.
input:checked + label {
color: blue;
}
Output👇
input:checked
: We are simply targeting the checked state of our input element using the pseudo-class and saying we want to do something when this element is checked.+
: This is an adjacent sibling selector, meaning we want to target an element that is immediately next to the input element.label
: Finally we are saying if the state of our input element changes to:checked
, change the color of the label element from whatever initial color to blue;
Easy as pie right? but there are two caveats (limitations) to using the state of one element to style another, which was why we had to make sure the input was above the label:
You can't style a parent or root element based on the state of its child, because CSS is a Cascading Style Sheet by nature, meaning styles are applied from the root of the document (top of the family tree) downwards to nested children. In otherward, only children can inherit properties from parents but never parents inheriting from children. Still confused? think of CSS as a waterfall that only flows downwards from the top of a fountain, you can't ask it to disrupt this flow by moving back up to change the style of an element based on a change in the state of a child element.
You can style an element based on the state of its sibling element, but only if the sibling element is an older sibling i.e. the element whose state is to be used has to be an element that sits above the target element in our markup. Using our example above, if our label sat above the input, we wouldn't be able to style the label based on the change in our input because the input is by order of arrangement, a younger sibling to the label. CSS will not carry styles upwards, only downwards, meaning only the state of an older sibling element (element that comes before) can affect a younger sibling (element that comes after).
Now you understand CSS a little better with that knowledge, you are ready to create a toggle switch using just CSS.
How to Create a Toggle Switch with Just CSS
The HTML
So let's begin with the HTML markup, but because we need at least two icons to implement the animated part of the switch, we are going to grab them off font awesome. If you happen to be a total newbie with little experience using font awesome icons, It's a library that gives you a collection of icons to use in your projects. We will begin by linking our HTML document to this library to get that access. The quickest means of doing this is by Googling "font awesome cdn" or using https://cdnjs.com/libraries/font-awesome.
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css
Add the gotten line of code to the head of your document just before the link to your CSS stylesheet. That's all, let's proceed with our HTML markup.
We'll start by recreating the input and label example we've been using since, but this time we'll put both siblings in a div container that acts as a wrapper, and instead of putting an actual text in the label, we will place two icons and a span as replacement for the label text.
<body>
<div class="switch-container">
<input type="checkbox" id="switch" />
<label for="switch">
<i class="fas fa-sun"></i>
<i class="fas fa-moon"></i>
<span class="ball"></span>
</label>
</div>
</body>
Output👇
That's all the HTML we need for this switch, but did you notice that when anything in the label tag (any icon) gets clicked, it also toggles our checkbox like we are clicking on the checkbox itself? Hopefully, it doesn't look like magic because we already explained this behaviour earlier
-
Hint: The labels
for
attribute is linked to the inputsid
.
The CSS
We'll start off by resetting our document and quickly centring everything in the DOM using flexbox.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
Now to the actual code, the switch container div that wraps the input and label is of little or no interest to us actually, we are mostly interested in styling the label
as a switch and using the input
elements change in state to do our little magic. So let's style our label before going through the code.
label {
display: flex;
width: 75px;
height: 35px;
justify-content: space-between;
align-items: center;
padding: 0 6px;
background: #222;
border-radius: 50px;
cursor: pointer;
position: relative;
}
Output👇
Because a label is an inline element, and we wish to give it a fixed width/height, we set its display to either in-line block or flexbox, I've gone with flex
so I can easily control the placement of the icons in the label by vertically aligning them in the center (align-items
) and then pushing them apart (justify-content
). Notice that the sun is not pushed to the end? If you recall, we have 3 elements inside our label, the two icons and the span element with a class of ball. CSS is simply respecting the invisible span as it has no content to show itself because it's an inline element. We then apply a little padding vertically (left/right) to keep the icons from touching the edge of the label, we proceed to give the label a dark background and a symmetrical radius of 50px so it looks just like a button/tab and finally a cursor so it's obvious it is meant to be clickable. But before leaving I throw in a position of relative (It's of no use right now, but we'll use it in a minute .)
Let's move our focus to the icons.
label i {
font-size: 18px;
}
label .fa-sun {
color: gold;
transition: 0.8s;
}
label .fa-moon {
color: #fff;
transition: 0.8s;
}
label .ball {
display: none;
}
Output👇
We've simply given both icons a size of 18px, then changed their individual colors to gold and white respectively and because we plan to move these Icons in a second, we give them a transition of 0.8s. We then hid the span element from the DOM temporarily because we are about to do something cool and I don't want that to get in the way, as a result, the sun is freely pushed to the end.
- Note: Transition property allows us to smoothly change the values of an element's property over a given duration.
It's time for our first magic, recall that when the label is clicked on, it triggers the checkbox to be toggled? We will now use that pseudo-class state to animate the icons to move each time the input is toggled.
input:checked + label .fa-sun {
transform: translateX(-43px);
}
input:checked + label .fa-moon {
transform: translateX(43px);
}
Output👇
-
input:checked + label .icons-class
: Again we're using a pseudo-class to target the checked state of the input, and then saying when this input is checked, target the next adjacent label (junior sibling) and do something to the icons inside that label. -
transform: translateX((43px)
: Here we are using the transform property which lets you change the disposition of an element (move, rotate, scale, translate, skew, etc.), and then applying a translateX which is to move the element horizontally. The moon is given -43px which is to move it to the left by 43px while the sun is to be moved to the right by 43 deg. Since we already set a transition duration, they'll move smoothly using the provided time.
For the next trick, we are going to make the moon disappear initially by setting its opacity to 0 and only make it visible when the input is checked, while the sun will be visible in its initial state but made to disappear when the input is checked.
label .fa-sun {
opacity: 1;
}
input:checked + label .fa-sun {
opacity: 0;
}
label .fa-moon {
opacity: 0;
}
input:checked + label .fa-moon {
opacity: 1;
}
Don't be confused by my choice of adding them as new CSS rules, I simply want you to be able to follow the order of added effects and not get confused by too much code in your face. Output👇
- Note: Opacity controls the transparency of an element. 0 = completely transparent, 1 = completely opaque (visible). Again, our transition duration makes this whole change appear smooth and animated. Now recall that we had a span with a class of ball next to our icons that we set to display: none? It's time for it to shine... we are going to style it like a round ball and give it a position of absolute... why?
Recall we gave the label element a position of relative? We did that because when a child elements position is set to absolute, it's pulled off its natural position and floats off in search of a parent element with a relative position to attach itself to. So when we set the span elements position to absolute, we pull it off the ground but still keep it in the labels bounding frame by setting the label to relative position. This way the ball doesn't affect our icons but rather floats above them.
label .ball {
position: absolute;
display: block;
width: 25px;
height: 25px;
top: 5px;
left: 5px;
background: #fff;
border-radius: 50%;
transition: 0.8s;
}
Output👇
As it is a span (inline), we made it a block element, added fixed with/height and a top/left property of 5px, meaning the ball should be held by 5px from the top and left sides of the label holding it. Then added a white background and made it circular using border-radius. You already know what the transition is there for, we are going to move this ball when the input gets toggled.
input:checked + label .ball {
transform: translateX(40px);
}
Output👇
Notice we no longer see the moon? That's because it's moving in the same direction as the ball so it's hidden behind it(overlapping). But that's not what we want, so we will modify our moon so it is on the right initial (its opacity still left at 0), and then move it to the left when the input is clicked (opacity still left at 1).
label .fa-moon {
transform: translateX(43px);
}
input:checked + label .fa-moon {
transform: translateX(0);
}
Output👇
We are almost done with our toggle switch, we'll change the background color of the ball and the label respectively when the toggle is checked. I intentionally set the transition duration to be long so we could see the changes that were happening to our elements, but now we'll speed it by setting every transition to .3s (0.3seconds) and finally hide the input that is hovering over the label.
label {
transition: 0.3s;
}
input:checked + label {
background: #c0c0c0;
}
input:checked + label .ball {
background: #222;
}
input {
display: none;
}
//Change every previous transition from .8s to .3s
Output👇
To make our switch more lively, let's throw in a rotation effect so the icons both spin out when toggled.
input:checked + label .fa-sun {
transform: translateX(-43px) rotate(160deg);
}
label .fa-moon {
transform: translateX(43px) rotate(250deg);
}
input:checked + label .fa-moon {
transform: translateX(0), rotate(0);
}
Final Result👇
Hurray!🥳🎉 We've made a sleek animated toggle switch with just CSS, and you've learnt enough to be able to recreate any toggle switch you come across on the internet. As a bonus. I'll link to a second article where we convert this toggle to two fancier ones made with svg icons.
Now, this article has gotten quite long, and for that reason, we're going to split it up and end with a cliffhanger here :). The rest of the article will focus on how to implement a dark theme or multiple themes on your pre-existing websites/projects or future ones. The link will be provided below, so also will a link for making two other variants of the toggle switch.
Conclusion
If you found this article helpful (which I bet you did 😉), got a question? or spotted an error/typo... do well to leave your feedback in the comment section. Lastly, someone might be somewhere struggling to make a toggle switch or implement a dark mode, do well to share this resource and follow me for more.
And if you're feeling generous (which I hope you are 🙂) or want to encourage me, you can put a smile on my face by getting me a cup (or thousand cups) of coffee below. :)
Link to Second Article
Posted on October 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.