It has been a long quest, for month if not years I was struggling with adapting menus in the CSS code and trying out JQueries that I don’t speak thus not understand and that didn’t work as explained. Always just patchworking my way to my desired goals.
Now I have finally made the important step towards my very own perfect horizontal, multi-level dropdown and responsive CSS menu – back to the very beginning. Stepping into the shoes of an absolute beginner, I studied the subject again from the very core. It was not the first time, but many years ago and forgetfulness made it necessary to dedust the pure coding knowledge that got nearly lost in too much WordPress theme-adaptions. But this time I have also taken the time to write all my remarks of Why and What into the CSS Code of the menu – for my future self and everyone out there that finds this post on his own quest towards the “perfect horizontal, multi-level dropdown and responsive CSS menu”. Let the story unfold…
Building the perfect horizontal, multi-level dropdown and responsive CSS menu – Step-by-step tutorial
Step 1: The backbone of every menu – an unordered list
Note: In this post I aim to explain everything to make it suitable even for the very beginners, because while searching for information in the net, I often came across explanations and code examples that required some side-knowledge or simply didn’t work as explained when copying them 1:1 into an own document. In case you come across such an event in this post as well, let me know in the comments so I can correct it.
Let’s have a look at the basic HTML, a simple unordered list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<!Doctype html> <html lang="de"> <head> <title>My quest for the perfect horizontal, multi-level dropdown and responsive CSS menu</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <link rel="stylesheet" href="style.css"> </head> <body> <header> <label for="responsive-button" class="responsive-button">Menu</label> <input type="checkbox" id="responsive-button" role="button"> <nav class="menu"> <ul> <li><a href="#">Home</a></li> <li><a href="#">Menu Item</a></li> <li><a href="#">Categories <span class="menu-arrow">▼</span></a> <ul> <li><a href="#">First Category</a></li> <li><a href="#">Longer Category</a></li> <li><a href="#">One more Category</a></li> <li><a href="#">Last Category</a></li> </ul> </li> <li><a href="#">Tags <span class="menu-arrow">▼</span></a> <ul> <li><a href="#">First Tag</a></li> <li><a href="#">Next Tag</a></li> <li><a href="#">Longer Tag</a></li> <li><a href="#">Last Tag</a></li> </ul> </li> <li><a href="#">Contact</a></li> </ul> </nav> </header> </body> </html> |
The label
and input
will create our responsive button that becomes visible instead of the menu in small screen sizes, and with which we can toogle the menu on and off. In this example the whole menu is working in pure CSS, and for toggling the menu an invisible checkbox is used. We will speak in the CSS section more about it.
By using a wrapping Nav
element with a class we can address the UL (unordered list) for the menu by using the .menu
class. You can use any other class-name or ID, but it is important to address the menu separated from the regular UL lists used in the content of your real site.
So each submenu has its own UL in its parent LI element. To the respective parents there is an arrow added that is styled using the .menu-arrow
class.
Because one gram of practice equals tons of theory, you should copy it to any text-editor, save as .html and open it in your browser to follow this tutorial. The plain HTML Code will look something like this:
Step 2: The CSS of our menu – getting horizontal
So we will start step by step, and you should create another text file in the same folder as your .html, calling it style.css. The explanations always follow after the code box.
1 2 3 4 5 6 7 8 9 10 |
html { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } *, *:before, *:after { -webkit-box-sizing: inherit; -moz-box-sizing: inherit; box-sizing: inherit; } |
We start with this small trick that gets us rid of the more complicated default box model of browsers, where any padding and borders are adding to the dimensions, so you need to re-calculate your width and height settings. For the easier work especially in the frame of this tutorial we simple set the box model to INCLUDE all padding and borders into the width, making it much easier. You can read more about it in this article.
1 2 3 4 5 6 7 |
.menu, .menu ul, .menu ul li, .menu ul li a, .menu ul ul, .menu ul ul li, .menu ul ul li a { margin: 0; padding: 0; border: 0; line-height: 1; } |
With this we reset our menu tags, at least if we don’t use a global reset that already covers it.
1 2 3 4 5 6 7 8 9 10 |
.menu ul { list-style-type: none; text-align: center; } .menu ul li { position: relative; display: inline; text-align: center; } |
In the final code which you can download at the end you will also find each CSS rule commented. There is also a rule following that you will only need if you use another method for hiding the submenu than we are using in this tutorial, in which we now use display: inline
instead of float: left
to align the menu items horizontally.
.menu ul
is addressing our main menu list.list-style-type: none
removes the bullets here.text-align: center
in combination with.menu ul li
set todisplay: inline
is centering the whole menu, which is looking great, but if you want to have it left-aligned, simply delete this line. Setting the LI elemtents to inline has one disadvantage though, since it creates a mysterious gap between them. In most designs this will not bother, but if it does, you find here a collection of solutions to remove the white-space. If you don’t want or cannot apply any of those workarounds, then you can center your menu also as described here, given that your menu has a fixed height. CSS is full of possibilities, but also limitations and their workarounds :)
- On
.menu ul li
we apply alsoposition: relative
which is later needed for theposition: absolute
of the submenu to refer to its parent LI element. display: inline
here not only is allowing the centering of the whole menu withtext-align: center
on its UL element, but also transforming our menu to a horizontal alignment. LI tags are by nature block elements, that means they take their own line. By transforming them into an inline element, they simply line up as words of a sentence. The other method to achieve this is usingfloat:left
, which doesn’t allow to center the whole menu as easily as applied here, and needs the clearing CSS rule that you find in the source code.text-align: center
here makes the A links centered inside their LI containers, which only makes sense if the A links have a fixed width, because otherwise they are anyhow centered in their box whose width is generated according to their content. To see all the effects of what is described here, simply delete those lines in the Codepen demo or your own text files to see what they do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.menu ul li a { text-decoration: none; display: inline-block; width: 200px; padding: 20px; color: #333; background: RoyalBlue; } .menu ul li a:hover, .menu ul li a:focus, .menu ul li a:active { color: #777; background: CornflowerBlue; } .menu-arrow { font-size: 10px; } |
- Now we style our
.menu ul li a
links, that are in the same time forming their own box. text-decoration: none
is needed to remove the underlining of links.display: inline-block
is needed to apply a fixed width or padding to A element, which by nature is an inline element that could not receive such. Inline-Block is a fabulous combination that allows the A links to still be inline next to each other while receiving those attributes.- We apply a fixed width in our example, which is not necessary and depends on your design (delete the line in Codepen and see the difference) and also a padding that gives the text space to breathe inside its box, which receives a royal background color. Also the text color will for sure be set in every menu you build.
- In the next CSS rule follows the styling of the hover, focus and active state, which can be set separately of course if desired.
- With the class of
.menu-arrow
we style the arrow that indicates a submenu, and which by default is too big.
Congratulations, we now already have a very nice one-level horizontal menu. If you delete the two submenu UL blocks under categories and tags in your HTML Code for a moment – presuming we don’t have submenu entries – your menu will now look like this:
Step 3: Multi-level Dropdown Menu – Level 2
Now let’s create our submenu structure, first sublevel. In the HTML we see that it is achieved by nested UL lists, so to address the submenu, we go one instance deeper using .menu ul ul
1 2 3 4 5 6 7 8 9 10 11 12 13 |
.menu ul ul { position: absolute; left: 0; opacity: 0; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* IE 8 */ filter: alpha(opacity=0); /* IE 5-7 */ } .menu ul li:hover ul { opacity: 1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; /* IE 8 */ filter: alpha(opacity=100); /* IE 5-7 */ } |
position: absolute
together withleft: 0
is placing the submenu entry directly below and at the left border of the parent element withposition: relative
, which we have set to the level 1 LI.- To hide the submenu by default and show it when hovering over it (respectively when touching it in the responsive menu on a touch-screen), there are different possibilities described below, each with its own advantages and disadvantages. In our example we use
opacity: 0
to hide the submenu andopacity: 1
on.menu ul li:hover ul
to show it (together with two workarounds for older IE versions, that we don’t want to skip because the menu function is absolutely vital for the usage of the website)
Hiding the submenu with display:none
The first idea that comes to mind when thinking about how to hide the submenu items.
1 2 3 4 5 6 7 |
.menu ul ul { display: none; } .menu ul li:hover ul { display: static; } |
Advantage:
- Besides being the first idea and easist solution, I coudn’t find any.
Disadvantage:
- It is not accessible and will disappear for screenreaders which is not good, but as found in this article, the accessibility rule of avoiding display: none for menus might sometimes be even harmful when it comes to very large sites.
- Another disadvantage might be that search engines could consider none-displayed links as a spam attempt, but I don’t know if this is true. If someone knows, please let us know in the comments.
- The submenu can not smoothly fade in using transitions.
Moving the submenu out of the visible screen with position:absolute and left:-9999px
We could set UL UL to left: -9999px
, which places the submenu simply far away outside of any screen, and by applying left: 0
on the .menu ul li:hover ul
we could bring it back, making it appear under our parent menu item. Pure magic. Also top, bottom or right can be used, but it makes no difference as long as you are not applying a transition, with which you could let the submenu slide in from one side. This creates a nice effect, but it breaks the logic of material design which is an amazing step of Google in improving the user experience in the web and should not be neglected. In short it says related to this aspect, that the submenu should originate in its appearance where it really comes from – from the parent menu item. Having it flying in from somewhere outside is misleading the experience by breaking the connection.
1 2 3 4 5 6 7 8 |
.menu ul ul { position: absolute; left:-9999px; } .menu ul li:hover ul { left: 0; } |
Advantage:
- It is accessible, since also screenreaders will still see the submenu items and they don’t disappear completely.
Disadvantage:
- The submenu can not smoothly fade in using transitions.
Using opacity to hide the submenu
This solution seems to be the perfect match without any disadvantages.
1 2 3 4 5 6 7 8 9 10 11 |
.menu ul ul { opacity: 0; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* IE 8 */ filter: alpha(opacity=0); /* IE 5-7 */ } .menu ul li:hover ul { opacity: 1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; /* IE 8 */ filter: alpha(opacity=100); /* IE 5-7 */ } |
Advantage:
- Fully accessible.
- The submenu can smoothly fade in using transitions.
Disadvantage:
- Besides the incompatibility in IE8 and older that can easily be fixed as shown, no problems known so far, leave yours in the comments if you know some.
Now let’s put some style on our submenu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
.menu ul ul li { display: block; text-align: left; } .menu ul ul li a { width: 100%; min-width:200px; padding: 20px; white-space: nowrap; border-bottom: 1px solid #ccc; color: #333; background: DodgerBlue; } .menu ul ul li a:hover, .menu ul ul li a:focus, .menu ul ul li a:active { color: #777; background: DeepSkyBlue; } .menu ul li a, .menu ul ul li, .menu ul li:hover, .menu ul ul { -webkit-transition: all .4s ease-in-out; transition: all .4s ease-in-out; } |
.menu ul ul li
are set back to block, so they are aligned vertically and not horizontally as their parents.- Also
text-align
is set back to left, because centered entries don’t look good in the submenu.
- Main styling again goes on
.menu ul ul li a
, whosewidth: 100%
is needed so the border goes to the end of the LI element which is giving the width based on the longest entry. min-width
is set to have a more consistent appearance if some submenu items would otherwise be very short.White-space
is set to nowrap so they stay in one line.border-bottom
is simply a nice thing in every vertical listing as in our submenu.
- Hover, focus and active states are set again to brighter colors.
- And the last CSS rule is putting the magic of smooth
transitions
instead of abrupt changes. Delete it and check the difference it makes, or play with the timing (I consider .4s for a perfect setting).
Now you should already have a handsome and smooth dropdown menu:
Step 4: Making the menu responsive
If we scale our browser window smaller, we will see that as soon as the elements doesn’t fit anymore in one line, they do not-so-beautiful things that for sure no webdesigner or website-visitor would like them to do. So let’s tell them to behave different under a certain browser width, in our case 768px.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@media screen and (max-width: 768px) { .menu ul li { display: block; border-bottom: 1px solid #ccc; } .menu ul li, .menu ul li a { width: 100%; } .menu ul ul { position: relative; display: none; } .menu ul li:hover ul { display: block; } .menu ul ul li { text-align: center; } } |
- We use the media query
@media screen and (max-width: 768px)
to toogle to the responsive version. This would be a bit early in live environment and it makes sense to scale the menu down before toogling, but for the moment we keep it like this.
.menu ul li
: we reset the menu item’s position to stack up vertically and add a bottom line for better separation.
.menu ul li, .menu ul li a
: we make all items full width.
.menu ul ul
: we need to reset toposition: relative
so the submenu is opening inside the element flow, not covering the following parent menu items. Delete the line to see yourself what it does otherwise.
- As a result, hiding them can not be done any longer with the left: -9999px method which relies on position: absolute, and also opacity doesn’t work here because it leaves an empty space between the parent items. Only
display: none
is left, so for the moment it seems that responsive dropdown menus can not be accomplished accessible using CSS only. If someone knows a solution, let us know in the comments.
text-align
on the LI is now set to center, because as centered entries didn’t look too good in a dropdown, left entries don’t look good in a responsive submenu.
Your responsive menu should now look like this:
Step 5: Creating a responsive button
It would be very uncommon to have the menu continously open on our mobile device, so let’s show instead a menu button. We will use a pure CSS solution, for which we have set in our HTML code a checkbox input field, that we will (mis-)use as our menu toogle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
.responsive-button { padding: 20px; color: #333; font-weight: bold; background: RoyalBlue; border-bottom: 1px solid #ccc; text-align: center; display: none; } input[id=responsive-button] { display: none; } input[id=responsive-button]:checked ~ .menu { display: block; } @media screen and (max-width : 768px){ .responsive-button { display: block; } .menu { display: none; } |
- The first CSS rule styles and hides the responsive button.
- With
input[id=responsive-button]
we address exactly this one input field, so no other inputs throughout your whole website will be affected. - This checkbox stays permanently hidden and we only use its functionality to toggle the menu on and off with the rule
input[id=responsive-button]:checked ~ .menu
which is showing again the .menu class (our whole nav element) that we are hiding in the media query, where we simultanously show the button, that we have been hiding in the regular menu display. Again, delete lines from the Codepen or in your text files and check which differences all these lines make to better understand the mechanism behind it.
And here we are: not yet perfect and multi-level, but a cross-browser working horizontal and responsive dropdown CSS menu.
The design is very basic, but it was not the aim of this tutorial, and also my own quest is heading towards the framework that I can easily use and adapt on all websites. But since you now learned to build it yourself, you also know exactly where to apply your styling ideas. You can use the above link to the Code-Pen or download the files to play around also locally.
The quest continues…
This was the first part of my quest so far, from Zero to a fully working menu, creating a purely CSS, horizontal and responsive two-level dropdown menu. The tasks left to reach the “perfect” and “multi-level” vision:
- Creating at least a third level depth
- Finding a way to not jump to the top of the page when clicking on a parent link in the responsive menu (that is set to # that’s why the page is jumping to the top “anchor”)
Integrating a triangle for parents that have children to make visible the existence of a submenuDone- Creating a nice small button with the hamburger icon
- Making the functionality nicer by integrating a handy JQuery solution
- Integrating it into WordPress (in best case without the need for any walker)
Hi! Thank you very much for this very useful tutorial! Your menu is working fine but I’d like to make it full width. Yours has a 200px width by default and it’s not fully responsive because it stays small on a full page. I tried to put ‘100%’ but it doesn’t seem to work. I’m not a coder sorry, so if you could help me it would be great, thank you! :)
Hey Armian, I am sorry but I didn’t play around with this for a long while now, I switched to basically just using the DIVI Theme where everything just works :) So more or less I gave up on developing my own theme. But I can briefly check your code if you send me the site where I can see the issue (send by mail if you don’t want it public here).