Hamburger Menu

First posted on December 27, 2025

The Hamburger Menu is a common mobile navigation pattern, but its implementation can be quite tricky. This post walks through a minimal, mostly native approach based on the <dialog> element.

The open button

The open <button> should live inside the <nav> landmark to define its purpose and enhance its discoverability.

It should also have:

  • type="button", otherwise it would default to "submit"Footnote 1;
  • a visually hidden text to provide an accessible name;
  • a title with the same value, as a nice addition for sighted usersFootnote 2;
  • aria-haspopup="dialog" to provide more information to assistive technologiesFootnote 3;
  • the icon (I’m using a character here for simplicity).
<button type="button" title="Open menu" aria-haspopup="dialog">
  <span class="visually-hidden">Open menu</span>
  <span aria-hidden="true">☰</span>
</button>

You could also add aria-expanded, but it would only makes sense if you are using a non-modal dialog.

In our case we will be using a modal dialog, which focus-traps the user and makes the rest of the page inert. This creates a new interaction context rather than expanding the existing one, so an expanded state wouldn’t be appropriate.

The modal dialog

The modal <dialog> should have:

  • an accessible name, provided via aria-label or aria-labelledby;
  • closedby="any" to enable light dismiss behaviorFootnote 4.

Inside it, as the first element, there should be a <button> for closing it. This can work with zero JavaScript if you enclose it in a <form> with method="dialog"Footnote 5.

Then comes the actual content of the <dialog>, which would be an exact copy of the list of links in your <nav> (but without the <nav> itself).

<dialog aria-label="Menu" closedby="any">
  <form method="dialog">
    <button title="Close menu">
      <span class="visually-hidden">Close menu</span>
      <span aria-hidden="true">×</span>
    </button>
  </form>

  <!-- Navigation links -->
</dialog>

With the structure in place, most of the work is done.

Opening it

In the near future, we will be able to open the dialog without a single line of JavaScript thanks to the Invoker Commands API (opens in a new window), which has just entered baselineFootnote 6. For now, let us sprinkle in a bit of JavaScript:

const menuToggle = document.getElementById("menu-toggle");
const menuDialog = document.getElementById("menu-dialog");

menuToggle?.addEventListener("click", () => menuDialog?.showModal());

Well that’s minimal indeed!

Styling it

Now it’s time to style it as a sidebar while blurring the content of the page beneath it:

.menu-dialog {
  position: fixed;
  height: 100%;
  width: 320px;
  max-width: 100%;
  left: auto;
}

.menu-dialog::backdrop {
  -webkit-backdrop-filter: blur(0.25rem);
  backdrop-filter: blur(0.25rem);
}

Animating it

But what about the animation? Here you go:

@media (prefers-reduced-motion: no-preference) {
  .menu-dialog,
  .menu-dialog::backdrop {
    transition:
      display 0.5s allow-discrete,
      overlay 0.5s allow-discrete,
      translate 0.5s,
      background-color 0.5s;
  }
}

.menu-dialog {
  translate: 100%;

  &::backdrop {
    background-color: rgb(0 0 0 / 0);
  }

  &[open] {
    translate: 0;

    &::backdrop {
      background-color: rgb(0 0 0 / 0.5);
    }
  }

  @starting-style {
    &[open] {
      translate: 100%;

      &::backdrop {
        background-color: rgb(0 0 0 / 0);
      }
    }
  }
}

I know I know, there is a bunch of weird stuff in there, but suffice you to know that the allow-discrete plus @starting-style combo is what’s needed to enable transitions on properties that don’t really have intermediate states (like display), and even if Firefox support is still lacking (at the time of writing), this is a textbook case of progressive enhancement, so no problem.

The final structure

<header>
  <nav aria-label="Primary">
    <!-- Navigation links -->

    <button type="button" title="Open menu" aria-haspopup="dialog">
      <span class="visually-hidden">Open menu</span>
      <span aria-hidden="true">☰</span>
    </button>

    <dialog aria-label="Menu" closedby="any">
      <form method="dialog">
        <button title="Close menu">
          <span class="visually-hidden">Close menu</span>
          <span aria-hidden="true">×</span>
        </button>
      </form>

      <!-- Navigation links -->
    </dialog>
  </nav>
</header>

What a gift! The web platform really handles most of the heavy lifting, and you end up with a nicely animated and accessible hamburger menu (with very little JavaScript code nevertheless).

Now go, be (and code) well!

Back to top