Before I start I want to say this post was inspired by @lkopacz's post.
Create custom keyboard accessible checkboxes
Lindsey Kopacz ・ Nov 27 '18
A lot of what I show here will be similar to her design pattern with a few of my twists, and of course, the end result is a switch, not a checkbox.
What do we want to create?
The aim is to make a material design inspired toggle switch that is compatible with keyboard interaction, We won't be using javascript because I don't think we need to for something so simple.
The markup
We're going to be using a standard checkbox as that comes with all the built-in a11y goodness, there's no need to reinvent the wheel. We'll be surrounding it with a label tag so we can interact with some text if we want, and also so we can interact with the toggle we'll be making.
I find it easier to have a span that we can style for our actual toggle. Here is the code I have settled on.
<label class="md_switch">
<input type="checkbox" />
<span class="md_switch__toggle"></span>
Toggle switch
</label>
If we were to have an issue with our CSS this would load a standard checkbox and would be perfectly usable, which is something we should always be striving for.
Styles
There are several states a toggle can be in and we'll have to have a style for each of these. The states are:
- Default/not checked
- Checked
- Disabled
- Focus
Default
Setup
Let's look at how we want the label to look first, you'll remember our label had the class .md_switch
. This is a material project so we'll want to use the Open Sans font, you don't have to but I will. We want our label to be inline but at the same time we want inside our element to use flexbox, so we'll set the display to inline-flex
. This means we can vertically align all the bits inside the element with align-items: center
which is a win for us. We'll also want to increase the top and bottom margins as our toggle will be a little bigger than a standard checkbox, I went for 5px.
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
.md_switch {
display: inline-flex;
font-family: "Open Sans";
align-items: center;
margin: 5px 0;
}
When we mouse over our toggle we'll want the cursor to show it's something we can click on so let's change the cursor to pointer.
.md_switch .md_switch__toggle {
cursor: pointer;
}
The toggle
Let's move onto making the actual toggle. There are 2 parts to a toggle like this the background layer and then the little nob/switch on top. The background lends itself to being a ::before
and the nob makes sense being the ::after
so that's simple enough.
Both the before and after will need to have a content value, a background colour, a margin and a transition effect. All the values will be the same so we'll put them together like so
.md_switch .md_switch__toggle::before,
.md_switch .md_switch__toggle::after {
content: '';
background: #BDBDBD;
margin: 0 3px;
transition: all 100ms cubic-bezier(0.4, 0.0, 0.2, 1);
display: block;
}
You'll notice I also added a display property, though it's not technically needed by both there's no harm in it being there.
I got the cubic-bezier and time from the material design spec, here.
Now we'll look at the background part of the toggle. Feel free to play with the size yourself but I'll explain why I went with these settings.
.md_switch .md_switch__toggle::before {
height: 1.3em;
width: 3em;
border-radius: 0.65em;
opacity: 0.6;
}
The height I've set at just over the line height so it feels like it fills the entire line, width is just over twice the height to make it feel long but not too long, border-radius is half height to make a pill shape and the opacity is 0.6 to make it appear further away than the switch bit, which will have an opacity of 1. These were just the values that felt right to me you can change them to your heart's content.
Finally, for this section, we'll put the switch on top. We want to position this absolute so we'll need to set the parent's position to relative, the parent is just .md_switch .md_switch__toggle
though so that's ok.
We want the switch to be circular, slightly taller than the background and centred vertically also, for a little depth, let's add a box shadow.
.md_switch .md_switch__toggle::after {
position: absolute;
top: 50%;
transform: translate(0, -50%);
height: 1.7em;
width: 1.7em;
border-radius: 50%;
box-shadow: 0 0 8px rgba(0,0,0,0.2), 0 0 2px rgba(0,0,0,0.4);
}
I used transform
this way to make it easier to animate later.
Checked
The next few bits are a lot easier, most of the legwork was done in the default view.
For the checked we want to change the background colour of both bits to our active
colour.
.md_switch [type=checkbox]:checked+.md_switch__toggle::before,
.md_switch [type=checkbox]:checked+.md_switch__toggle::after{
background: #00897B;
}
but we also want to move our switch to the other side of the background pill. If we moved the nob over the entire length of the pill and back by its own width we should be right against the right edge a transform like calc(3em - 100%)
ought to do it. And it does.
.md_switch [type=checkbox]:checked+.md_switch__toggle::after {
transform: translate(calc(3em - 100%), -50%);
}
Disabled
If the element is disabled we want to somehow show the user they can't interact with it. What I like to do is add a grayscale filter and lower the opacity but also remove the shadow from the after element to make it look flat.
We can also set the cursor to not-allowed
for mouse users to get the message.
.md_switch [type=checkbox]:disabled+.md_switch__toggle {
cursor: not-allowed;
filter: grayscale(100%);
opacity: 0.6;
}
.md_switch [type=checkbox]:disabled+.md_switch__toggle::after {
box-shadow: none;
}
Focus
This is the final state we need to account for when the checkbox is focused we want to indicate to the user that they've got our element selected. This is the exact same code used by @lkopacz because it works so well.
.md_switch [type=checkbox]:focus+.md_switch__toggle {
outline: #5d9dd5 solid 1px;
box-shadow: 0 0 8px #5e9ed6;
}
Finishing up
We've made the toggle it works and looks, I think, quite pretty but we've still got the original checkbox we need to hide. In the past, many people would do a display none but this is a terrible idea, it totally hides the element and takes it out of the tab index meaning keyboard users totally lose the element. So what can we do?
There are loads of ways to hide an element and keep it in the tab index. My personal prefered method is to make it take up no space, lower the opacity and to turn off pointer events but you can use any method you like.
.md_switch [type=checkbox] {
position: absolute;
opacity: 0;
pointer-events: none;
}
Feel free to tell me what you'd do different in the comments below, I look forward to hearing what you all think.