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:
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>
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);
}
}
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>
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;
}
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>
And the CSS is about as simple as can be!
header {
position: relative;
&:has(.hamburger input[type="checkbox"]:checked) .menu {
width: 300px;
}
}
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!