Modern CSS hamburger using :has()

Charles Loder - Jul 22 - - Dev Community

I know, I know...yet another CSS hamburger tutorial. There's already hundreds out on the internet to copy-and-paste. Why bother with another one?

Besides the basest reason — I want to do it myself, this tutorial is a little different in that it uses the new :has() selector, which allows us to implement a type of "controller pattern" for our CSS.

Jump to the finished product below to see the Codepen demo.

The problem with most hamburgers

Perusing through the various hamburger menu tutorials online, I tend to see a few problems (or rather, things I just don't like):

  • They rely on Javascript. That isn't the worst (I love JS), but we can handle this logic using just CSS.
  • They weirdly nest things. Depending on how the HTML is structured, the actual menu can be nested under the hamburger itself. It's just hard to reason about
  • They are tightly coupled. Complex CSS selectors often mean that the functionality is tightly coupled to the structure of the HTML. While there has to be some coupling, I think there is a better way

Using :has() to create a controller.

Honestly, this image pretty much says it all:

An infographic displaying a header being used as a controller object

In short, the header is a controller. When the state of the .hamburger changes, the state of the .menu is updated.

But let's go through all the main parts to get a better sense of what's happening.

The hamburger

The hamburger HTML is pretty straightforward:



<div class="hamburger">
  <input type="checkbox" id="toggle">
  <label for="toggle" class="icon">
    <span class="bg-white"></span>
    <span class="bg-white"></span>
  </label>
</div>


Enter fullscreen mode Exit fullscreen mode

The CSS is a little more, but still not complicated:



.hamburger {
  height: 32px;
  width: 32px;

  input[type="checkbox"] {
    display: none;
  }

  .icon {
    width: 100%;
    height: 100%;
    display: block;
    position: relative;
    cursor: pointer;
  }

  .icon span {
    display: block;
    position: absolute;
    height: 3px;
    width: 100%;
    border-radius: 3px;
    opacity: 1;
    left: 0;
    transform: rotate(0deg);
    transition: 300ms ease-in-out;
  }

  .icon span:nth-child(1) {
    top: 33%;
  }

  .icon span:nth-child(2) {
    top: 66%;
  }

  input[type="checkbox"]:checked + .icon span:nth-child(1) {
    top: 50%;
    transform: rotate(45deg);
  }

  input[type="checkbox"]:checked + .icon span:nth-child(2) {
    top: 50%;
    transform: rotate(-45deg);
  }
}


Enter fullscreen mode Exit fullscreen mode

By using nesting and input[type="checkbox"] we can create reusable CSS. We can create a second hamburger with a different id for the input without the causing a conflict:



<div class="hamburger">
  <input type="checkbox" id="accordion-toggle">
  <label for="accordion-toggle" class="icon">
    <span class="bg-black"></span>
    <span class="bg-black"></span>
  </label>
</div>


Enter fullscreen mode Exit fullscreen mode

This also allows the hamburger to only care about its own state. You can just drop it in to your site, and without a controller object, the spans will animate, but that's it.

The menu

I won't paste the whole .menu HTML here, but it's a div with a nav, nothing special.

The CSS isn't much either



.menu {
  position: absolute;
  top: 0;
  left: 0;
  /* hide the menu by setting the width to 0 */
  /* we can't animate from display:none to width:auto yet… */
  width: 0;
  height: 100dvh;
  transition: width 300ms ease-out;
  overflow: hidden;
}


Enter fullscreen mode Exit fullscreen mode

We position it absolutely (in this case, we'll position the header so the menu attaches to that) to the top and the left and animate its width. This will cause it to animate from left to right.

The header

The HTML for the header has a logo (optional), the hamburger and the menu:



<header class="flex justify-between px-4 py-6 bg-gray-500">
  <div class="logo">...</div>
  <div class="hamburger">...</div>
  <div class="menu">...</div>
</header>


Enter fullscreen mode Exit fullscreen mode

And the CSS is about as simple as can be!



header {
  position: relative;
  &:has(.hamburger input[type="checkbox"]:checked) .menu {
    width: 300px;
  }
}


Enter fullscreen mode Exit fullscreen mode

Using the :has() selector allows up to check the state of one child to update the state of another.

Reusability

One advantage of this type of menu, besides the ridiculously easy CSS, is how reusable it is.

Another hamburger can be placed anywhere without affecting other menus. In the Codepen below, I added a accordion element using the same hamburger.

This works really well in component based frameworks where you can just use a <Hamburger /> component inside a controller and wire it all up.

Finished project

Here is the whole thing.

I used Tailwind for non-critical styling, and all the relevant CSS for the .hamburger and parts is in the CSS file.

Areas for improvement

One major area for improvement is accessibility. The example isn't very accessible.

As mentioned before, turning the hamburger into a reusable component could greatly improve the portability of it.

Let me know what you think!

. . . . . . . . . . . . . . . . . . . . . .