DEV Community

loading...
Cover image for Creating a fold out navigation with CSS only

Creating a fold out navigation with CSS only

Cyd
Freelance Web developer
Updated on ・4 min read

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.

Opening and closing a fold out navigation

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 :) */
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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; 
}

Enter fullscreen mode Exit fullscreen mode

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.

Discussion (39)

Collapse
jlrxt profile image
Jose Luis Ramos T.

Excelente y gracias por compartir.

Collapse
twinappz profile image
TabbRon

Glad you liked it.

Collapse
imcanada profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
imcanada

yo stupid, she speaks dutch and english. stop this spanish bs in the comment sections of english websites.

Collapse
cydstumpel profile image
Cyd Author

Calm down, Canada.

Thread Thread
imcanada profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
imcanada

just doesnt make sense why theyre speaking spanish.

btw great article

Thread Thread
alan5142 profile image
Alan Ramírez Herrera

Maybe he doesn't know english, there's no need to be rude.

Great article =)

Thread Thread
adnanbabakan profile image
Adnan Babakan (he/him)

Great article.
BTW this website isn't restricted about languages. Any one can speak their own language.

Thread Thread
joebeurg profile image
Youssef C.

Actually it is, I've read it somewhere

Thread Thread
andy profile image
Andy Zhao (he/him)

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.

Thread Thread
imcanada profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
imcanada

ok æðislegt það er fullkomlega eðlilegt að ég sé að svara ummælum á ensku á íslensku.

doesnt help anyone though does it?

Thread Thread
cydstumpel profile image
Cyd Author

Take the feedback, Canada.

Collapse
xirclebox profile image
Homer Gaines

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!!

Collapse
cydstumpel profile image
Cyd Author

Thank you!!! This is exactly what I was looking for. We might have to add border: none and background: 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!

Collapse
xirclebox profile image
Homer Gaines • Edited

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>

Collapse
cydstumpel profile image
Cyd Author

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!

Collapse
xirclebox profile image
Homer Gaines

Yeah, I noticed that too! facepalm

I made a codepen with the updates

codepen.io/xirclebox/pen/jOEGyvv?e...

Thread Thread
cydstumpel profile image
Cyd Author

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!

Collapse
lampewebdev profile image
Michael "lampe" Lazarski

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.

Collapse
cydstumpel profile image
Cyd Author

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.

Collapse
lampewebdev profile image
Michael "lampe" Lazarski

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 ;)

Collapse
scottyferreira profile image
Scott

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

Collapse
cydstumpel profile image
Cyd Author

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

Collapse
andrewfreites profile image
andrewfreites

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

Collapse
andrewfreites profile image
andrewfreites

I get it, the "left" attribute of ul. Thank you, is an interesting escenario

Collapse
cydstumpel profile image
Cyd Author

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 🙂

Collapse
hnnx profile image
Nejc

Awesome code :)

Collapse
samuelonoja profile image
Samuelonoja

This is dope...Cyd
Love it ❤🤗

Collapse
winniebosy profile image
Winnie Magoma

Nice one.. I must try this out

Collapse
jspiderhand profile image
Justin Stout

Great article! I saved it to my reading list for later review!!

Collapse
kmwill23 profile image
Kevin

My big take away here is using a custom checkbox input to maintain the menu state. Perfect. Oh, and your fancy menu is amazing 😁

Collapse
kp profile image
KP • Edited

Nice!

Collapse
dabeatsmith profile image
dB Smith • Edited

Would you be able to link a button element to the input?
I may play around with the idea later but to be semantic seems like we could wrap the input in a button parent element

Collapse
cydstumpel profile image
Cyd Author

Unfortunately no, the for attribute only works on the label element and that’s how we link the input and label. Also if you wrap the input in an element you can no longer use sibling selectors to show and hide the navigation 😔.

Collapse
abdul_maliekk profile image
Abdulmaliekk

It's creative. Keep it ip

Collapse
lilarmstrong profile image
Armstrong

Lovely Tutorial @cydstumpel

Collapse
ibjorn profile image
Björn Potgieter

Very cool, thanks!

Just something I noticed. I played with this on a basic one page site with anchors. When clicking a link, the menu stays open. So you need to close it to view the content.

Collapse
cydstumpel profile image
Cyd Author

Oh damn, you found the loophole, this might actually take some JavaScript

Collapse
hinasoftwareengineer profile image
Hina-softwareEngineer

Nice article

Collapse
piotrek profile image
Piotr

I like it!, Mary Jane :)