How to make a hamburger menu without JavaScript
Short answer
A hidden checkbox stores the "open" state, a label controls it, and :has() shows the menu:
<style>
.nav { padding: 8px; background: navy; color: white; }
.nav input { position: absolute; opacity: 0; }
.nav label { cursor: pointer; font-size: 24px; }
.nav input:focus-visible + label { outline: 2px solid orange; }
.nav .links { display: none; flex-direction: column; gap: 6px; padding-top: 8px; }
.nav:has(input:checked) .links { display: flex; }
.nav a { color: white; }
</style>
<nav class="nav">
<input type="checkbox" id="burger" aria-label="Menu"><label for="burger">☰</label>
<div class="links"><a href="https://example.com">Home</a><a href="https://example.com">Shop</a><a href="https://example.com">About</a></div>
</nav>Only on mobile
On a wide screen the button is not needed — the menu is always shown:
@media (min-width: 700px) {
.nav label { display: none; }
.nav .links { display: flex; flex-direction: row; }
}A popover variant
The popover attribute gives a menu above the page, closing on Esc and an outside click, and a button with popovertarget tells screen readers whether the menu is open:
<button popovertarget="side" aria-label="Menu">☰</button>
<nav id="side" popover style="margin: 0; height: 100%; padding: 16px;"><a href="https://example.com">Home</a><br><a href="https://example.com">Shop</a></nav>Common mistake
People hide the checkbox with display: none, and then the menu cannot be opened from the keyboard. Hide it visually, as in the first example, and give it an aria-label, since the "☰" icon tells a screen reader nothing.
Functions used in this recipe
- <input type="checkbox"> — A checkbox is an independent yes/no choice; a radio button picks one option from a group.
- :has() — Matches an element that contains something matching the selector in brackets — the "parent selector".
- :checked — Matches a checked checkbox or radio button, and a selected `<option>`.
- <label> — A label for a form field.