Simple Mobile Menu Button

Tue Oct 31 2023

A delicious looking cheeseburger

In this post I’m going to show you how to create this hamburger-style mobile menu button:

But first, why are they called hamburger buttons?

Background

[skip]

Norm Cox, the American designer credited with inventing the hamburger button, created it for the Xerox Star personal computer, which was released in 1981.

”Its graphic design was meant to be very ‘road sign’ simple, functionally memorable, and mimic the look of the resulting displayed menu list.”

So the icon was designed to evoke the image of a list of menu or settings items; it’s only coincidence that it looks a little bit like a hamburger. However, the nickname really stuck and likely inspired a whole host of other food-named menu icons.

Neat! So how do we code it?

For starters, you don’t have to code anything. You could download an SVG or PNG image and place it inside a <button> tag. But I am a sucker for subtle animations, which is the sole reason I prefer the method I’m going to show you.

HTML

The HTML for this is super simple—just two elements with id attributes so we can access them with CSS and JavaScript/TypeScript.

A note about accessibility: It’s important and it shouldn’t be an afterthought. This isn’t an alternative to reading up on web accessibility, but a cheat code you can use is this: if you’re creating some type of component—in our case a mobile menu button—visit a trusted, well-respected site like w3.org or developer.mozilla.org and see if they have a similar component somewhere. If they do, open up your dev tools and see how they built it, paying attention to things like keyboard-interactivity.

<button
  type="button"
  id="burger-btn"
  aria-haspopup="menu"
  aria-label="Open menu"
  aria-expanded="false"
>
  <div id="bars"></div>
</button>

CSS

The CSS for this is, by far, the most complex part. It relies heavily on CSS custom properties and pseudo-elements which are fun to work with.

Note: you could absolutely perform the following calculations yourself and hard-code these values. I designed it this way so that I could play around with sizes and ratios (and because the sliders are fun).

Let’s start with the #burger-btn selector:

#burger-btn {
  /* Custom Properties */
  --menu-bar-width: 24px;
  --menu-bar-thickness: 3px;
  --menu-bar-gap: 5px;
  --actual-gap: calc(var(--menu-bar-gap) + var(--menu-bar-thickness));
  /* Style Declarations */
  border: none;
  padding: 1rem 0.5rem;
  background-color: transparent;
  cursor: pointer;
}

The 4 style declarations are straightforward. We want to:

  1. Override the default border so our website doesn’t look like it was made in the 90’s.
  2. Add some extra padding—which looks nice, but also gives the button a larger hit box making it easier to tap.
  3. Make the background transparent.
  4. Change the user’s cursor to indicate that the button is an interactive element. Usually this style of button only appears on mobile devices, but it’s nice to add this in case a mouse-user is viewing our page on a small screen.

The custom properties will be used to style and animate the bars. The --actual-gap calculation ensures that even with a gap of 0px, the bars will touch but not overlap.

Here’s the CSS that styles their closed state:

#bars,
#bars::before,
#bars::after {
  position: relative;
  display: block;
  width: var(--menu-bar-width);
  height: var(--menu-bar-thickness);
  background-color: #22232a;
  border-radius: 1000px;
  transition: all ease 300ms;
}

#bars::before {
  content: "";
  position: absolute;
  bottom: var(--actual-gap);
}

#bars::after {
  content: "";
  position: absolute;
  top: var(--actual-gap);
}

If you’re using a css preprocessor, PostCSS, or you’re reading this in a future where native CSS nesting is widely supported, I encourage you to nest these styles as it makes them much more readable.

The three bars that make up the hamburger icon are represented by three different elements:

  1. The top bar is the #bars::before pseudo element.
  2. The middle bar is the actual <div> element.
  3. The bottom bar is the #bars::after pseudo element

They each share the same base styles like, width, height, color, radius, etc. The middle “parent” element has position: relative while the two pseudo-elements override this to position: absolute so that their positioning is relative to their parent. I think CSS position property values are confusing and perhaps poorly named, so here’s a reference.

It’s important to ensure that they all have display: block otherwise the pseudo-elements won’t be aligned with the central <div> element.

The --actual-gap custom property is used set the distance of each pseudo-element from the parent element using the top and bottom properties.

Now here’s the CSS that styles their open state:

#bars.open {
  background-color: transparent;
}

#bars.open::before {
  --transY-distance: var(--actual-gap);
  transform: translateY(var(--transY-distance)) rotate(45deg) scaleX(1.25);
}

#bars.open::after {
  --transY-distance: calc(-1 * var(--actual-gap));
  transform: translateY(var(--transY-distance)) rotate(-45deg) scaleX(1.25);
}

We introduce an open class selector here to differentiate between states.

It’s worth reiterating that the hamburger icon can remain a hamburger whether the menu is open or closed. This won’t negatively impact user experience, in my opinion. However, I like it when hamburger icons become X’s to indicate that they now serve the function of closing the menu. Two things need to happen in order to turn our hamburger into an X:

  1. At least one of the bars needs to disappear.
  2. Two of the bars need to rotate 45 degrees in opposite directions.

And that’s what these three blocks accomplish. First we make the middle bar transparent. Then we use transform with translateY() and rotate() to bring the two pseudo elements back to the center and rotate them.

The scaleX() function is optional, but helps to maintain the overall size of the button by lengthening the rotated bars. If you think about a 1x1 square, a line that runs diagonally from corner to corner would have a length of roughly 1.41. Through trail and error, I landed on 1.25. I just think it looks nice.

JavaScript / TypeScript

This is really minimal—just enough to trigger the button’s animation and update the aria attributes. If you’re using this button to toggle a mobile menu, you should handle the opening and closing inside the event listener callback as well.

const burgerBtn = document.querySelector<HTMLButtonElement>("#burger-btn")!;
const bars = document.querySelector<HTMLDivElement>("#bars")!;

burgerBtn.addEventListener("click", () => {
  bars.classList.toggle("open");

  const ariaExpanded = burgerBtn.getAttribute("aria-expanded");
  if (ariaExpanded === "false") {
    // Menu is going from CLOSED to OPEN
    burgerBtn.setAttribute("aria-expanded", "true");
    burgerBtn.setAttribute("aria-label", "Close menu");
  } else {
    // Menu is going from OPEN to CLOSED
    burgerBtn.setAttribute("aria-expanded", "false");
    burgerBtn.setAttribute("aria-label", "Open menu");
  }
  /**
   * Logic for showing/hiding
   * mobile menu goes here...
   */
});

Well, that’s how I create pretty much all of my mobile menu buttons. Thanks for reading!


View the MDX for this page or submit an issue if you noticed any errors!