Skip to main content

Breakdown and build of a typical hamburger menu complete with animations using CSS.

Usability and accessibility first

First and foremost the hamburger icon isn't a universal symbol for a menu, even the term hamburger isn't widely known. I've seen reports state more clicks may be obtained by using the word Menu. Though I personally believe it's dependent upon your target audience. This site, for instance, is aimed at web-devs and not the general public, so the meaning should be clear.

The recommendations I've read state a minimum click space of 34px square, but read Ideal Mobile Touchscreen Target Sizes for a better insight.

Accessibility suggests making every click-able area as large as possible, and the button must scale with font size, as set by the user, to at least 200%. This example scales beautifully.

Usability recommends buttons should be within thumb reach on mobile devices, but I'll leave placement decisions to you. Personally I've chosen to ignore that advice for now.

As this is the main menu button, which may hover over content, I've extended it to 48px for use here.

Semantic HTML

Well it does something rather than goes somewhere so semantically it's a button. Here's the example:

Language HTML
<button id=menu_example class=btn-menu aria-pressed="false">
  <svg class=svg-menu width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
    <title>Open</title>
    <path class="h t" d="M10.5 10l17 0"/>
    <path d="M10.5 19l17 0"/>
    <path class="h b" d="M10.5 28l17 0"/>
    <path class="x" d="M19 10.5l0 17"/>
  </svg>
  <span>Menu</span>
</button>

The SVG contains three horizontal paths, plus one vertical and are completely controlled via the CSS.

Without CSS, there's nothing there so be sure to add a span to contain the important text, then move it off-screen.

The pretty pretty - button styling.

Language CSS
.btn-menu {
  display: block;
  position: relative;
  padding: 0;
  background-color: #FAFAFD;
  border: 1px solid rgba(96, 96, 128, 0.1);
  border-radius: 50%;
  box-shadow: 0 0.25em 0.25em rgba(0, 0, 0, 0.3);
  transition: box-shadow 0.8s ease-out, background-color 0.8s ease-out;
}
.btn-menu:hover,
.btn-menu:focus {
  background-color: #fff;
  box-shadow: 0 0.5em 0.5em rgba(0, 0, 0, 0.4);
}
.btn-menu:active {
  background-color: #fff;
  box-shadow: none;
}

Optionally fix the button's position so it remains static as the user scrolls the content. For example to the top right.

Language CSS
.btn-menu {
  …
  position: fixed;
  top: 0.25rem;
  right: 0.25rem;
  z-index: 5;
}

A ::before pseudo class adds a highlight ring.

Language CSS
.btn-menu::before {
  content: "";
  display: block;
  position: absolute;
  width: 100%;
  height: 100%;
  border: 1px solid #fff;
  border-radius: 50%;
}

The SVG styling

While I personally make extensive use of rem, here I purposefully use em to allow the button to scale dependent upon font-size inherited from the cascade.

Language CSS
.svg-menu {
  display: block;
  width: 2.38em;
  height: 2.38em;
  margin: 0.25em;
  stroke-width: 5;
  stroke-linecap: square;
  stroke: #D9E4F6;
  transition: stroke 0.8s ease-out, transform 0.8s ease-in-out;
}
.btn-menu:hover .svg-menu,
.btn-menu:focus .svg-menu {
  stroke: #418cec;
}

Accessible text

Allthough other methods are available, I prefer to simply move the text off-screen as this method works even on older screen-readers.

As the SVG contains the buttons action, open or close. It's best to add an aria-labeledby attribute and point it to the SVGs title. Unfortunately I couldn't get it to validate with it in place.

Language CSS
.btn-menu span {
  position: absolute;
  text-indent: -200rem;
}

Scripting kept simple

Use the aria-pressed attribute to control the switch state via JavaScript.

Language JavaScript
(function () {
  var menuBtn = d.getElementById("menu_example");

  function clicked() {
    var btn = this,
        svg,
        svgTitles,
        svgTitle;

    requestAnimationFrame(function() {
      btn.setAttribute("aria-pressed", "false" === btn.getAttribute("aria-pressed"));
      svg = btn.getElementsByTagName("svg");
      if (svg) {
        svgTitles = svg[0].getElementsByTagName("title");
        if (svgTitles) {
          svgTitle = svgTitles[0];
          if ("false" === btn.getAttribute("aria-pressed")) {
            svgTitle.textContent = "Open";
          } else {
            svgTitle.textContent = "Close";
          }
        }
      }
    });
  }
  if (menuBtn) {
    menuBtn.addEventListener("click", clicked, false);
  }
}());

The menu used on this site is far more complex. I'll delve into that in another article.

Finally the CSS animations

The whole SVG rotates clockwise.

Language CSS
.btn-menu[aria-pressed="true"] .svg-menu {
  transform: rotate(225deg);
}

The two outside paths move toward the centre and fade out, while the cross fades in.

Language CSS
.svg-menu path {
  backface-visibility: hidden;
  transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
}
.svg-menu .x,
.btn-menu[aria-pressed="true"] .h {
  opacity: 0;
}
.btn-menu[aria-pressed="true"] .x {
  opacity: 1;
}
.btn-menu[aria-pressed="true"] .t {
  transform: translate(0, 8px);
  transform: translateY(8px);
}
.btn-menu[aria-pressed="true"] .b {
  transform: translate(0, -8px);
  transform: translateY(-8px);
}

Socialise: