2026-05-07

Accessible Menu

What is the best way to make a menu accessible? This is what I have been asking myself a lot over the past year since I have dived deep into the WCAG guidelines. Before we start coding, it might be a good idea to make a list of what an accessible menu looks like.



It is important that the menu is progressively enhanced. This means that the menu is build in layers with the idea that when one layer fails to load, the menu will still work. You could think of the following layers: HTML, CSS and JavaScript.

I have been thinking about how to prevent tabbing outside of an open mobile menu. A solution for this might be using a dialog. But on desktop I don't want the menu to be inside of a dialog. This means that I would need two separate menu's, one for mobile and one for desktop. But there is another issue we need to think about when using dialog: we need JavaScript to open and close the dialog. For now, my solution to this issue would be to use the dialog[open] styling for when the dialog is triggered with :target. By doing this, the dialog will not be on the top layer of the page. But this way of displaying the dialog is only an exception for when JavaScript is turned off. To target the dialog we would need an anchor link within <noscript> tags. When JavaScript is enabled, we want to use the regular button to trigger the dialog to open.

Take a look below at my attempt to make an accessible menu. I wrote some comments in the code to explain what is happening. I made this menu in svelte. If you have any questions about this code, don't hasitate to contact me on LinkedIn.



<script>
    import { onMount } from 'svelte';

    onMount(async () => {
        const menuButton = document.getElementById('buttonOpenDialog');
        const closeMenu = document.getElementById('buttonCloseDialog');
        const dialogMenu = document.getElementById('dialogMenu');
        const dialogLinks = document.querySelectorAll('dialog a');

        // check if js is enabled
        document.documentElement.classList.add('js-enabled');

        menuButton.addEventListener('click', () => {
            dialogMenu.showModal();
        });

        closeMenu.addEventListener('click', () => {
            dialogMenu.close();
        });

        // close dialog after clicking a link
        dialogLinks.forEach((link) => {
            link.addEventListener('click', () => {
                dialogMenu.close();
            });
        });
    });
</script>

<header class="header-top">
    <nav>
        <ul class="mobile-menu">
            <li>
                <a href="/" class="header-astronaut">
                    <figure>
                        <img src="/astronaut-navigation.webp" alt="astronaut" loading="lazy" />
                        <figcaption>Logo</figcaption>
                    </figure>
                </a>
            </li>

            <!-- mobile noscript element for when js isn't working -->
            <noscript>
                <li>
                    <a href="#dialogMenu" class="a-open-dialog">
                        <span class="earth" aria-hidden="true">🌍</span>
                        <span>Menu</span>
                    </a>
                </li>
            </noscript>

            <!-- mobile button for when js is enabled -->
            <li class="li-button-open-dialog">
                <button class="button-open-dialog" id="buttonOpenDialog">
                    <span class="earth" aria-hidden="true">🌍</span>
                    <span>Menu</span>
                </button>
            </li>
        </ul>

        <ul class="desktop-menu-items">
            <li><a href="/">Home</a></li>
            <li><a href="/">Blogs</a></li>
            <li><a href="/">Projects</a></li>
        </ul>
    </nav>
</header>

<dialog id="dialogMenu">
    <header class="dialog-header">

        <!-- close button for when js isn't working -->
        <noscript>
            <a href="#closeDialogMenu" class="a-close-dialog" title="Close menu">
                <span class="saturn" aria-hidden="true">🪐</span>
                <span>Close</span>
            </a>
        </noscript>

        <!-- close button when js is enabled -->
        <button class="button-close-dialog" id="buttonCloseDialog" title="Close menu">
            <span class="saturn" aria-hidden="true">🪐</span>
            <span>Close</span>
        </button>
    </header>

    <nav>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/">Blogs</a></li>
            <li><a href="/">Projects</a></li>
        </ul>
    </nav>
</dialog>

<style>
    .header-top {
        position: fixed;
        top: 0;
        width: 100dvw;
        height: 4.5rem;
        background-color: var(--color1);
        z-index: 999;

        & .mobile-menu {
            list-style: none;
            margin-top: 0;
            padding: 0 1rem;
            height: 4.5rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
    }

    .header-astronaut {
        color: black;
        text-decoration: none;

        & figure {
            position: relative;
            margin: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            width: fit-content;
        }

        & img {
            height: 2.5rem;
        }

        figcaption {
            font-size: 0.7rem;
            font-weight: bold;
        }
    }

    /* hide the desktop menu items on mobile */
    .desktop-menu-items {
        display: none;
    }

    /* js not enabled = display none; js-enabled = display flex */
    .li-button-open-dialog {
        display: none;
    }

    :global(html.js-enabled) {
        .li-button-open-dialog {
            display: flex;
        }
    }

    /* Dialog styling & button styling */
    dialog {
        position: fixed;
        margin: 0;
        padding: 0;
        top: 0;
        display: flex;
        flex-direction: column;
        width: 100dvw;
        max-width: none;
        height: 100dvh;
        max-height: none;
        background-color: var(--color1);
        border-color: var(--color1);
        transform: translateX(100%);
        transition: transform 0.3s ease-in-out;
        z-index: 999;

        & ul {
            margin: 0;
            padding: 5rem 0;
            list-style: none;
            display: none;
            flex-direction: column;
            gap: 2rem 0;
            text-align: center;

            & a {
                font-size: 2rem;
                font-weight: bold;
                color: black;
                text-decoration: underline var(--color3);

                &:hover,
                &:focus {
                    color: var(--color3);
                    text-decoration: underline var(--color4);
                }
            }
        }
    }

    .button-close-dialog,
    .a-close-dialog {
        display: none;
    }

    .button-open-dialog,
    .a-open-dialog {
        display: flex;
    }

    .button-open-dialog,
    .a-open-dialog,
    .button-close-dialog,
    .a-close-dialog {
        position: relative;
        padding: 0;
        flex-direction: column;
        align-items: center;
        border: none;
        font-size: 0.7rem;
        font-weight: bold;
        color: black;
        background: transparent;
        text-decoration: none;

        &:hover {
            cursor: pointer;
        }
    }

    .button-open-dialog { gap: 0.2rem; }
    .button-close-dialog { gap: 0rem; }

    .earth {
        position: relative;
        top: -0.5rem;
        height: 2.5rem;
        font-size: 2.4rem;
    }

    .saturn {
        position: relative;
        top: -0.5rem;
        height: 3rem;
        font-size: 3rem;
    }

    :global(html.js-enabled):has(dialog[open], dialog:target) {
        .button-close-dialog {
            display: flex;
        }
    }

    dialog[open],
    dialog:target {
        transform: translateX(0);

        & .a-close-dialog { display: flex; }
        & ul { display: flex; }
    }

    @starting-style {
        dialog[open] {
            transform: translateX(100%);
        }
    }

    .dialog-header {
        padding: 0 1rem;
        display: flex;
        justify-content: end;
    }

    @media screen and (min-width: 64em) {
        dialog { display: none; }
        .button-open-dialog { display: none; }

        nav {
            display: flex;
            justify-content: space-between;
            width: calc(80dvw);
            margin: auto;
        }

        .desktop-menu-items {
            margin-top: 0;
            padding: 0 1rem;
            height: 4.5rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 2rem;
            list-style: none;
        }

        .desktop-menu-items a {
            font-size: 1.3rem;
            font-weight: bold;
            color: black;
            text-decoration: underline var(--color3);
        }

        .desktop-menu-items a:hover,
        .desktop-menu-items a:focus {
            color: var(--color3);
            text-decoration: underline var(--color4);
        }
    }
</style>

I am interested in learning the best way to make an accessibile menu for as many people as possible. The menu I made above is not perfect, perhaps using dialog for a mobile menu isn't even a good option. I would like to know your opinions about this.

Go back to blogs