Springe zum Inhalt
griechisches lambda

Mobile Navigation Menus without JavaScript

By Paul Hempel, 25.10.2023

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.

Table of Contents

  1. The Core Implementation 
  2. Adding a Hamburger Menu Icon
  3. Animations
  4. Custom Layouts
  5. Visual Variations

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.

About the author

Paul Hempel

Paul lives in Mainz, Germany and works mainly as Clojure developer on websites and mobile apps. His topics include accessible web-design, teaching programming and finding out what good makes a good software developer.