Mobile Navigation Menus without JavaScript
To make our company website as accessible as possible, we wanted to present central functions without JavaScript. For this reason, we explored the question of whether a mobile navigation menu can be implemented using only HTML and CSS.
The Core Implementation
The complete code can be found as a Gist on GitHub.
On the HTML side, we keep the navigation quite simple. In the <nav>
section, there are two lists, one of which is enclosed by a collapsible details block for mobile view:
<nav>
<!-- desktop -->
<ul>
<li><a href="/">About</a></li>
<li><a href="/">Blog</a></li>
<li><a href="/">Contact</a></li>
</ul>
<!-- mobile -->
<details>
<summary aria-label="open mobile menu"></summary>
<ul>
<li><a href="/">About</a></li>
<li><a href="/">Blog</a></li>
<li><a href="/">Contact</a></li>
</ul>
</details>
</nav>
On desktop, we don't want to see the details
tag and its contents, while in the mobile view, we don't want the desktop menu:
nav>details {
display: none;
}
@media (max-width: 991px) {
/* hide desktop menu */
nav>ul {
display: none !important;
}
/* show mobile menu */
nav>details {
display: block !important;
}
}
Done?!
Our solution now meets all the technical requirements for a responsive menu. Visually, we want to further enhance the result in the next section.
Adding a Hamburger Menu Icon
Most of the time, we see a hamburger menu icon and a close icon on mobile versions of websites. Of course, we don't want to miss this here - and it should be implemented using only CSS:
/* custom image for mobile menu */
nav > details > summary::after {
content: '';
background-image: url(img/menu.svg);
background-size: 2rem 2rem;
display: inline-block;
width: 2rem;
height: 2rem;
}
/* alternative image for open mobile menu */
nav > details[open] > summary::after {
background-image: url(img/close.svg);
}
/* icons used: https://ionic.io/ionicons */
The details[open]
attribute allows us to check whether <details>
is open or closed. When it's open, in our case, we replace the background image. Since we work with the ::after
pseudo-element, we want to ensure that the icon doesn't take up the whole page. By combining background-size
, display: inline-block;
, width: 2rem;
and height: 2rem;
, we can ensure that the icon has our desired size.
Animations
Since we can't select parent nodes with CSS, we use a little trick that adds a subtle animation. Animations aren't strictly necessary, but since, in my opinion, this isn't an obvious solution, let's briefly look at what's possible.
nav>details>summary {
[...]
/* slight transition */
transition: margin-bottom 150ms ease-out;
}
/* add margin for slight transition */
nav>details[open]>summary {
margin-bottom: 1rem;
}
Since we can't access a parent element with CSS selectors (yet?) to animate it as a whole, we add a small margin-bottom
in the open state of the dialog
, which grows from 0rem
to 1rem
in 150ms using the transition
attribute.
Custom Layouts
The exact layout is subject to individual requirements. Therefore, this article won't go into further detail. Nonetheless, we have enriched the Gist with code for a proper layout variant as an example.
Visual Variations
A Fixed Header
The existing code lets the navigation bar scroll with the entire website. However, if it should always be visible, we can fixate it as follows:
header {
background-color: aliceblue;
display: flex;
justify-content: space-between;
/** toggle fixed header **/
position: fixed;
width: 100%;
}
main {
/* don't forget to increase the top padding with fixed navbar */
padding: 4rem 1rem;
}
A Fixed Header in Full-Screen
So far, the header
in its expanded state is only as large as its content. If this isn't desired and it should instead fill the entire page, we can influence it as follows:
nav>details>ul {
flex-direction: column;
align-items: end;
margin: 0;
/** toggle full page menu **/
height: 100vh;
}
One Known Issue
Due to the absence of so-called "Focus-Trapping", the page in the background can continue to scroll. This can only be prevented by JavaScript (as of 2023). Therefore, we decided to use a fixed header for our website, which isn't full-screen.