16 CSS Side Menu Designs 13 / 16

Hamburger Checkbox-Hack Side Menu

A classic pure-CSS interaction using a hidden checkbox input and the :checked general sibling combinator to toggle menu visibility and animate the burger icon into an X.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="sm-13">
  <input type="checkbox" id="sm-13-chk">
  <label class="sm-13__burger" for="sm-13-chk" aria-label="Toggle menu">
    <span></span><span></span><span></span>
  </label>
  <!-- Clicking overlay also closes the menu -->
  <label class="sm-13__overlay" for="sm-13-chk"></label>
  <nav class="sm-13__nav">
    <div class="sm-13__links">
      <a class="sm-13__link sm-13__link--active" href="#"><span class="sm-13__link-chip"></span> Dashboard</a>
      <a class="sm-13__link" href="#"><span class="sm-13__link-chip"></span> Analytics</a>
      <a class="sm-13__link" href="#"><span class="sm-13__link-chip"></span> Content</a>
      <a class="sm-13__link" href="#"><span class="sm-13__link-chip"></span> Audience</a>
      <div class="sm-13__divider"></div>
      <a class="sm-13__link" href="#"><span class="sm-13__link-chip"></span> Help</a>
      <a class="sm-13__link" href="#"><span class="sm-13__link-chip"></span> Settings</a>
    </div>
    <div class="sm-13__user">
      <div class="sm-13__avatar">CK</div>
      <div>
        <div class="sm-13__uname">C. Kim</div>
        <div class="sm-13__urole">Publisher</div>
      </div>
    </div>
  </nav>
  <div class="sm-13__content">
    <div class="sm-13__heading">Checkbox Hack Menu</div>
    <div class="sm-13__sub">Uses a hidden <code>&lt;input type="checkbox"&gt;</code> and the CSS sibling combinator. No JavaScript at all.</div>
    <div class="sm-13__code-card">
      <span class="sel">#chk:checked ~ .nav</span> {<br>
      &nbsp;&nbsp;<span class="kw">transform</span>: translateX(0);<br>
      }<br>
      <span class="sel">#chk:checked ~ .burger span:nth-child(1)</span> {<br>
      &nbsp;&nbsp;<span class="kw">transform</span>: translateY(8px) rotate(45deg);<br>
      }
    </div>
    <div class="sm-13__grid">
      <div class="sm-13__card"><div class="sm-13__card-val">0 JS</div><div class="sm-13__card-lbl">Lines of JS</div></div>
      <div class="sm-13__card"><div class="sm-13__card-val">100%</div><div class="sm-13__card-lbl">Pure CSS</div></div>
    </div>
  </div>
</div>
.sm-13, .sm-13 *, .sm-13 *::before, .sm-13 *::after {
  box-sizing: border-box; margin: 0; padding: 0;
}
.sm-13 ::selection { background: #f97316; color: #fff; }
.sm-13 {
  --bg: #111113;
  --surface: #1c1c1f;
  --nav-bg: #18181b;
  --accent: #f97316;
  --accent2: #fb923c;
  --text: #f4f4f5;
  --muted: #71717a;
  --border: rgba(249,115,22,0.15);
  --font: 'Rubik', system-ui, sans-serif;
  --w: 270px;
  --dur: 0.38s;
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  position: relative;
  overflow: hidden;
  border-radius: 12px;
}
/* Hidden checkbox — the :checked hack engine */
#sm-13-chk { display: none; }
/* ======== BURGER LABEL ======== */
.sm-13__burger {
  position: absolute;
  top: 18px; left: 18px;
  z-index: 100;
  width: 44px; height: 44px;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 6px;
  cursor: pointer;
  border-radius: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  transition: background 0.2s;
}
.sm-13__burger:hover { background: rgba(249,115,22,0.15); }
/* Three bars — animated via :checked ~ sibling combinator */
.sm-13__burger span {
  width: 20px; height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: transform var(--dur) cubic-bezier(0.4,0,0.2,1), opacity var(--dur), width var(--dur);
  transform-origin: center;
  display: block;
}
/* :checked ~ .burger targets the burger because both are children of .sm-13 */
#sm-13-chk:checked ~ .sm-13__burger span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
#sm-13-chk:checked ~ .sm-13__burger span:nth-child(2) { opacity: 0; width: 0; }
#sm-13-chk:checked ~ .sm-13__burger span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }
/* ======== OVERLAY (backdrop close area) ======== */
.sm-13__overlay {
  position: absolute; inset: 0;
  z-index: 40;
  pointer-events: none;
  transition: background var(--dur);
}
#sm-13-chk:checked ~ .sm-13__overlay {
  background: rgba(0,0,0,0.5);
  pointer-events: all;
}
/* ======== DRAWER ======== */
.sm-13__nav {
  position: absolute;
  top: 0; left: 0;
  width: var(--w);
  height: 100%;
  background: var(--nav-bg);
  border-right: 1px solid var(--border);
  z-index: 50;
  transform: translateX(calc(-1 * var(--w)));
  transition: transform var(--dur) cubic-bezier(0.4,0,0.2,1);
  display: flex;
  flex-direction: column;
  padding: 74px 0 20px;
}
#sm-13-chk:checked ~ .sm-13__nav {
  transform: translateX(0);
  box-shadow: 8px 0 32px rgba(0,0,0,0.5);
}
/* Orange top accent line */
.sm-13__nav::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--accent), var(--accent2), transparent);
}
.sm-13__links { flex: 1; padding: 0 10px; }
.sm-13__link {
  display: flex; align-items: center; gap: 13px;
  padding: 12px 14px;
  border-radius: 9px;
  color: var(--muted);
  font-size: 14px; font-weight: 600;
  cursor: pointer; text-decoration: none;
  transition: all 0.18s;
  margin-bottom: 4px;
}
.sm-13__link:hover { color: var(--text); background: rgba(249,115,22,0.1); }
.sm-13__link--active { color: var(--accent2); background: rgba(249,115,22,0.12); }
.sm-13__link-chip {
  width: 8px; height: 8px;
  background: var(--muted);
  border-radius: 2px;
  transition: background 0.18s;
  transform: rotate(45deg);
  flex-shrink: 0;
}
.sm-13__link:hover .sm-13__link-chip { background: var(--accent2); }
.sm-13__link--active .sm-13__link-chip { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
.sm-13__divider { height: 1px; background: var(--border); margin: 8px 14px; }
.sm-13__user {
  padding: 16px 18px 0;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px;
}
.sm-13__avatar {
  width: 32px; height: 32px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--accent), #ef4444);
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: #fff;
}
.sm-13__uname { font-size: 13px; font-weight: 700; }
.sm-13__urole { font-size: 10px; color: var(--muted); }
/* ======== MAIN CONTENT ======== */
.sm-13__content {
  padding: 80px 22px 22px;
}
.sm-13__heading { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.sm-13__sub {
  font-size: 13px; color: var(--muted);
  line-height: 1.7; margin-bottom: 22px;
}
/* Explain the checkbox hack technique */
.sm-13__code-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  font-size: 12px;
  font-family: monospace;
  color: #a1a1aa;
  line-height: 1.8;
  margin-bottom: 14px;
}
.sm-13__code-card .kw { color: var(--accent); }
.sm-13__code-card .sel { color: #60a5fa; }
.sm-13__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.sm-13__card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 13px;
}
.sm-13__card-val { font-size: 20px; font-weight: 700; color: var(--accent2); }
.sm-13__card-lbl { font-size: 11px; color: var(--muted); margin-top: 3px; }
@media (prefers-reduced-motion: reduce) {
  .sm-13__nav, .sm-13__overlay, .sm-13__burger span { transition: none; }
}

How this works

The checkbox-hack relies on #id:checked ~ .target selecting siblings that follow the checked input in the DOM. The hidden <input type="checkbox">, the burger label, the backdrop label, and the nav panel are all direct children of the wrapper so the ~ combinator reaches all from a single checked state.

The three burger bars transform individually using :nth-child() — top bar rotates +45°, middle fades out, bottom rotates −45°, all sharing var(--dur) for perfect sync. A second label covering the full viewport acts as a click-outside-to-close zone using pointer-events: all only when checked.

Customize

  • Turn the X into a splayed asterisk by using rotate(135deg) and rotate(-135deg) instead of ±45°.
  • Add an opening sound effect via the Web Audio API triggered on the checkbox change event — purely additive and degrades gracefully.
  • Animate the middle bar width to 0 before opacity 0 for a more sequential animation.
  • Use @keyframes instead of transitions on the nav for a spring overshoot: translateX(-20px) at 80% then translateX(0) at 100%.
  • Add swipe-to-close by mirroring a touch drag offset to the nav translateX via a small JS event listener.

Watch out for

  • The ~ combinator only targets elements after the checkbox in the DOM — keep the checkbox as the first child of its parent.
  • If inside a <form>, form submission will include the checkbox value — keep the input outside any form elements.
  • iOS VoiceOver treats the checkbox as a form control and announces it — add aria-hidden="true" to the input and aria-expanded on the burger label via JS for full accessibility.

Browser support

ChromeSafariFirefoxEdge
26+ 7+ 20+ 26+

The :checked sibling selector pattern works in all modern browsers and IE9+. No prefixes required.

Search CodeFronts

Loading…