While using JavaScript to open a mobile navigation is a solid option, it's also possible to do it with CSS only. Even submenus don't necessarily need any JS to work, pseudo classes and sibling selectors are the answer!
How
Fold out navigations are usually triggered by what's been lovingly called the hamburger menu, referring to the three horizontal lines. This has been a much debated icon, but people have gotten used to it, so for now let's stick to it.
To create a CSS only navigation we need to use an <input />
element with type="checkbox"
, we will use the :checked
pseudo class to see if it's selected. And we need a <label>
that's linked to the input element with a for
attribute.
This is the simplified version of what I usually create, for clarity's sake I've styled on elements rather than classes:
<nav>
<input type="checkbox" id="trigger" />
<label aria-label="open main navigation" for="trigger"><span></span></label>
<ul>
<li>
<a href="#">Link</a>
</li>
<li>
<a href="#">Link</a>
</li>
<li>
<a href="#">Link</a>
</li>
</ul>
</nav>
Note that I put the label element after the input element, this makes it possible to style the label by the input's state (:focus
, :checked
, etc).
Hiding the checkbox
The checkbox input needs to be entirely gone. I added position fixed because the rest of the navigation is fixed and standard behaviour of clicking on a checkbox label is scrolling to it, which would mean scrolling to the top.
input {
opacity: 0;
width: 0;
height: 0;
appearance: none;
position: fixed;
}
Creating the trigger
You can just add a background image of a hamburger icon here or you can use before and after selectors to create them. This is the visible part of the trigger, this will be your "nav is closed!"-state.
You might have wondered about the 'unnecessary' span
inside the label
tags, I added it to make it styling it easier, you can leave the span out.
label {
position: fixed;
top: 0;
right: 0;
z-index: 10;
display: flex;
align-items: center;
width: 15px;
height: 10px;
margin: 40px;
}
label span {
display: block;
width: 100%;
background: hotpink;
height: 2px;
transition: transform 1s;
}
label span::before,
label span::after {
content: "";
position: absolute;
left: 0;
right: 0;
display: block;
height: 2px;
background: hotpink;
transition: 0.4s
}
label span::before {
top: 0;
}
label span::after {
top: calc(100% - 2px); /* to make the transition easier :) */
}
Hiding the navigation
While you could just add display: none
here, and display: block
when you want to show it, using a transform is a lot more fun.
I chose to translate the ul
100vw on the X axis to hide it from the screen until the user clicks on the trigger.
ul {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: translateX(100vw);
transition: transform 0.2s;
background: white;
}
Creating the 'open state'
I talked earlier about using the :checked pseudo class, a pseudo class is a keyword that's added to an element when it has a certain state.
Radio- and checkbox input types have a :checked
state. Checkboxes can be checked and unchecked by clicking on them, in order to select another radiobutton there needs to be another button. Checkboxes are more suitable for our goals so we use them.
This is the place to add the css for the nav trigger's open state, a lot of websites don't style and animate this and it's a shame in my opinion.
The code may look intimidating if you're new to css but if you read it out loud it sometimes helps to understand it more clearly. The first line for example: select the inner element 'span' of the sibling 'label' of the checked 'input'.
Okay that still sounded a bit like gibberish, practice really does make perfect here.
input:checked + label span {
transform: rotate(45deg);
}
input:checked + label span::before, input:checked + label span::after {
top: calc(50% - 1px);
transition: 0.4s;
}
input:checked + label span::after {
transform: rotate(90deg);
}
Showing the navigation
I used the general sibling selector here, this means "select any ul that is a sibling of a checked input", input:checked + label + ul
would also have worked, but is more sensitive to errors (what if you add an element in between?).
input:checked ~ ul {
transform: none;
transition: transform 0.4s;
}
The pretty SCSS version with beziers and all the fancy stuff:
But why?
Because we can!
This may seem a lot more complicated than just adding a class with JavaScript and you may be right. This is not only a great exercise if you want to get better at handling pseudo classes and sibling selectors, it's also very sturdy. Imagine your website has a small error in the JS, this would mean people couldn't use your navigation anymore. Let's leave JavaScript for the crazy animations and keep simple things like opening and closing a navigation as a CSS thing.
Accessibility
A good point to throw away this entire article is that using an input element to open and close a navigation is not semantic.
But really if you think about it, most people using screen readers (the group that might notice that you used an input
element in stead of a more semantic button
element) aren't exactly developers, you might really annoy the blind developers though, be ware.
The trigger is automatically accessible with the keyboard, if you also add a clear aria-label
you've done more than most developers who use a button
to open a fold out navigation.
I would really love some input from actual blind users or accessibility experts, let me know in the comments whether I'm completely clueless.
Top comments (38)
Excelente y gracias por compartir.
Glad you liked it.
Calm down, Canada.
just doesnt make sense why theyre speaking spanish.
btw great article
Maybe he doesn't know english, there's no need to be rude.
Great article =)
Great article.
BTW this website isn't restricted about languages. Any one can speak their own language.
Actually it is, I've read it somewhere
DEV doesn't have any restrictions on languages, and we are working to actively support them.
@imcanada comments and replies should be constructive, respectful, and show a level of empathy toward others. Please take some time and review our Code of Conduct.
ok æðislegt það er fullkomlega eðlilegt að ég sé að svara ummælum á ensku á íslensku.
doesnt help anyone though does it?
Take the feedback, Canada.
Hey Cyd,
First off, very nice work. I really like the CSS only approach you took. My only concern was as you pointed out at the end that your example is not semantic. As a result, using the wrong element can trigger the wrong browse mode in assistive tech which can be a bit confusing. But in this case, using the checkbox as a toggle might be ok. I need to do some more digging.
Aside from that, I tested your example using NVDA with Firefox and Chrome. Things worked great when I wasn't using my screen reader. However, I found that when using my screen reader and the hamburger menu was in focus, the checkbox was not clickable (Chrome). I also noticed that when in focus, NVDA didn't announce the purpose of the element(chrome and Firefox). So I took a look at the code to see what was going on and I found a few things that fixed the issues in both Chrome and Firefox.
Removing the height, width and appearance from .nav__trigger-input allowed for the checkbox to be clicked via the keyboard when the hamburger menu was in focus(Chrome). And by placing the aria-label on the input element, when in focus, NVDA announced, "open the navigation. not checked" (Chrome). Placing an aria-labeledby="trigger" and changing the for="trigger" on the label to id="trigger" allowed for the aria-label to be correctly announced by NVDA in both Chrome and Firefox.
.nav_trigger-input,
.nav_submenu-trigger-input {
opacity: 0;
position: fixed;
}
< input
class="nav_trigger-input"
type="checkbox"
id="trigger"
aria-label="click to open the navigation"
aria-labelledby="trigger"
/>
< label class="nav_trigger-finger" id="trigger">
I hope my suggestion helps.
Again, great work!!
Hmmm I can actually use the keyboard to open the navigation in chrome, using tab and space to open. You also shouldn't use the same ID twice as they should be unique so Im going to call it something different. Still thanks for your help!
Yeah, I noticed that too! facepalm
I made a codepen with the updates
codepen.io/xirclebox/pen/jOEGyvv?e...
Hahaha looks great, I think the problem is that not the entire hamburger is clickable, I usually just use a svg to animate between states, but that seemed a bit too complicated for this article.
Great work!
Thank you!!! This is exactly what I was looking for. We might have to add
border: none
andbackground: none
because for some weird reason checkboxes are really hard to make invisible in all browsers. I’m going to add this code en reference you in a bit!No prob :)
I made an update. I noticed the explicit label while associated with the labeled by was not triggering the mouse click. So I tied the label to the input using hamburger as the id and for. Also added cursor: pointer; to .nav__trigger-finger.
< nav class="nav">
< input
class="nav_trigger-input"
type="checkbox"
id="hamburger"
aria-label="click to open the navigation"
aria-labelledby="trigger"
/>
< label class="nav_trigger-finger" id="trigger" for="hamburger">
< span>
Just a few findings from me:
1) The animation on a 27 inch display is a little bit to much. I personally find it not very pleasant after the second time
2) On mobile I can see the "C" from "contact"
3) On Firefox I have 2 scroll bars.
4) The Hamburger menu does not animate from 1 line to 3 lines. It just jumps to 3 lines.
In general I see this screen filing menus more and more. This to me only makes sense on Mobile where the space is limited but on a laptop or even on a 27 inch monitor it is for me not a very good user experience.
Besides that good article.
Hahaha you think I would actually use this in production? This is just to showcase what you can do with pseudo classes, don't take things too seriously.
I'm not taking it too serious :)
Maybe some (junior) devs will think that this should be used in production.
Like people use the codedrops examples in production and then the complete UI is super laggy ;)
Great article! Thank you! May I make a suggestion for future articles? Can you please show the browser window after you show the code snippets? It makes following along easier, and also makes it easier to know if there's an error. I went through your article and have something wrong. Unfortunately I don't know where in the code I made the error. Still a great article! keep it up
Oops, there was a mistake in the code; the li tags weren't closed, that might have thrown you off, here is the simple version of the navigation as shown in the article:
codepen.io/Sidstumple/pen/JjorPWB
Nice one.. I must try this out
Awesome code :)
This is dope...Cyd
Love it ❤🤗
I understand the goal of this, but Wich value I need to modify to use only 50%,30%, etc, of screen after translate the ul? Thank you
I get it, the "left" attribute of ul. Thank you, is an interesting escenario
That’s a great option, you could also not set the left property and give it a width of 30vw (I would add a min width in pixels as well to keep it all readable) for example 🙂
I like it!, Mary Jane :)
My big take away here is using a custom checkbox input to maintain the menu state. Perfect. Oh, and your fancy menu is amazing 😁
Some comments may only be visible to logged-in visitors. Sign in to view all comments.