28 CSS Hamburger Menus09 / 28
Pure CSS Hamburger Menu Without JavaScript
The checkbox hack, rebuilt for the :has() era: the input still holds the state, but :has(:checked) reads it from the ROOT, so the burger, the header, the fullscreen panel and even the page content can all react — no sibling-order gymnastics, no restructured markup. Compare demo 05 (the ~ combinator version) side by side: same zero-JS guarantee, but here the menu tints the header, scales the page and swaps the burger, all from one selector.
Published Updated
The code
<section class="chm-09" aria-label="Pure CSS :has() hamburger menu demo">
<div class="chm-09__stage">
<input type="checkbox" id="chm09-t" class="chm-09__state">
<div class="chm-09__page" aria-hidden="true">
<div class="chm-09__hero"><b>:has()</b><span>state, readable anywhere</span></div>
<div class="chm-09__ln" style="--w:82%"></div>
<div class="chm-09__ln" style="--w:60%"></div>
<div class="chm-09__ln" style="--w:73%"></div>
</div>
<nav class="chm-09__panel" aria-label="Site">
<a href="#index" style="--i:0">Index</a>
<a href="#essays" style="--i:1">Essays</a>
<a href="#library" style="--i:2">Library</a>
<a href="#colophon" style="--i:3">Colophon</a>
</nav>
<header class="chm-09__bar">
<span class="chm-09__brand">marginalia</span>
<label for="chm09-t" class="chm-09__burger"><span aria-hidden="true"><i></i><i></i><i></i></span></label>
</header>
</div>
</section><section class="chm-09" aria-label="Pure CSS :has() hamburger menu demo">
<div class="chm-09__stage">
<input type="checkbox" id="chm09-t" class="chm-09__state">
<div class="chm-09__page" aria-hidden="true">
<div class="chm-09__hero"><b>:has()</b><span>state, readable anywhere</span></div>
<div class="chm-09__ln" style="--w:82%"></div>
<div class="chm-09__ln" style="--w:60%"></div>
<div class="chm-09__ln" style="--w:73%"></div>
</div>
<nav class="chm-09__panel" aria-label="Site">
<a href="#index" style="--i:0">Index</a>
<a href="#essays" style="--i:1">Essays</a>
<a href="#library" style="--i:2">Library</a>
<a href="#colophon" style="--i:3">Colophon</a>
</nav>
<header class="chm-09__bar">
<span class="chm-09__brand">marginalia</span>
<label for="chm09-t" class="chm-09__burger"><span aria-hidden="true"><i></i><i></i><i></i></span></label>
</header>
</div>
</section>.chm-09,
.chm-09 *,
.chm-09 *::before,
.chm-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.chm-09 {
background: var(--bg);
width: 100%;
min-height: 100vh;
box-sizing: border-box;
--bg: oklch(0.965 0.008 85);
--ink: oklch(0.24 0.02 60);
--mut: oklch(0.52 0.02 60);
--acc: oklch(0.58 0.16 45);
--panel: oklch(0.24 0.03 55);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
display: grid;
justify-content: center;
}
.chm-09__stage {
position: relative;
width: min(400px,88cqi,88vw);
height: 540px;
border-radius: 24px;
overflow: hidden;
background: var(--bg);
border: 1px solid oklch(0.87 0.015 80);
}
.chm-09__state {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.chm-09__bar {
position: absolute;
inset: 0 0 auto 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
padding: 0 16px 0 24px;
}
.chm-09__brand {
font-size: 16px;
font-weight: 700;
font-style: italic;
letter-spacing: -.01em;
transition: color .35s;
}
.chm-09:has(#chm09-t:checked) .chm-09__brand {
color: oklch(0.95 0.015 80);
}
.chm-09__burger {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--ink);
color: oklch(0.96 0.01 80);
cursor: pointer;
transition: background .3s,scale .2s;
}
.chm-09__burger:hover {
scale: 1.06;
}
.chm-09__state:focus-visible ~ .chm-09__bar .chm-09__burger {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger {
background: oklch(0.96 0.01 80);
color: var(--panel);
}
.chm-09__burger span {
position: relative;
width: 18px;
height: 12px;
display: block;
}
.chm-09__burger i {
position: absolute;
left: 0;
width: 18px;
height: 2px;
border-radius: 2px;
background: currentColor;
transition: translate .3s,rotate .3s .05s,opacity .2s;
}
.chm-09__burger i:nth-child(1) {
top: 0;
}
.chm-09__burger i:nth-child(2) {
top: 5px;
}
.chm-09__burger i:nth-child(3) {
top: 10px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(1) {
translate: 0 5px;
rotate: 45deg;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(2) {
opacity: 0;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(3) {
translate: 0 -5px;
rotate: -45deg;
}
.chm-09__page {
position: absolute;
inset: 0;
padding: 86px 24px 24px;
display: grid;
gap: 14px;
align-content: start;
transition: scale .5s cubic-bezier(.2,.7,.2,1),filter .5s,opacity .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__page {
scale: .96;
filter: blur(2px);
opacity: .55;
}
.chm-09__hero {
height: 190px;
border-radius: 16px;
background: linear-gradient(150deg,oklch(0.9 0.035 70),oklch(0.84 0.06 45));
display: flex;
flex-direction: column;
justify-content: end;
padding: 18px;
margin-bottom: 6px;
}
.chm-09__hero b {
font-size: 30px;
letter-spacing: -.02em;
font-family: ui-monospace,Menlo,Consolas,monospace;
}
.chm-09__hero span {
font-size: 12.5px;
color: var(--mut);
}
.chm-09__ln {
height: 11px;
width: var(--w);
border-radius: 99px;
background: oklch(0.89 0.012 80);
}
.chm-09__panel {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(170deg,color-mix(in oklch,var(--panel) 92%,white),var(--panel) 60%);
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 32px;
opacity: 0;
scale: 1.04;
visibility: hidden;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel {
opacity: 1;
scale: 1;
visibility: visible;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s 0s;
}
.chm-09__panel a {
padding: 16px 2px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: oklch(0.95 0.015 80);
text-decoration: none;
border-bottom: 1px solid oklch(1 0 0/.12);
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0;
translate: 0 14px;
transition: color .2s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}
.chm-09__panel a::after {
content: "→";
font-size: 18px;
color: var(--acc);
opacity: 0;
translate: -8px 0;
transition: opacity .2s,translate .25s;
}
.chm-09__panel a:hover::after,
.chm-09__panel a:focus-visible::after {
opacity: 1;
translate: 0 0;
}
.chm-09__panel a:hover,
.chm-09__panel a:focus-visible {
color: oklch(0.85 0.09 60);
}
.chm-09__panel a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 4px;
border-radius: 4px;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel a {
opacity: 1;
translate: 0 0;
transition-delay: 0s,calc(.14s + var(--i)*.06s),calc(.14s + var(--i)*.06s);
}
@media (prefers-reduced-motion: reduce) {
.chm-09 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}.chm-09,
.chm-09 *,
.chm-09 *::before,
.chm-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.chm-09 {
background: var(--bg);
width: 100%;
min-height: 100vh;
box-sizing: border-box;
--bg: oklch(0.965 0.008 85);
--ink: oklch(0.24 0.02 60);
--mut: oklch(0.52 0.02 60);
--acc: oklch(0.58 0.16 45);
--panel: oklch(0.24 0.03 55);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
display: grid;
justify-content: center;
}
.chm-09__stage {
position: relative;
width: min(400px,88cqi,88vw);
height: 540px;
border-radius: 24px;
overflow: hidden;
background: var(--bg);
border: 1px solid oklch(0.87 0.015 80);
}
.chm-09__state {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.chm-09__bar {
position: absolute;
inset: 0 0 auto 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
padding: 0 16px 0 24px;
}
.chm-09__brand {
font-size: 16px;
font-weight: 700;
font-style: italic;
letter-spacing: -.01em;
transition: color .35s;
}
.chm-09:has(#chm09-t:checked) .chm-09__brand {
color: oklch(0.95 0.015 80);
}
.chm-09__burger {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--ink);
color: oklch(0.96 0.01 80);
cursor: pointer;
transition: background .3s,scale .2s;
}
.chm-09__burger:hover {
scale: 1.06;
}
.chm-09__state:focus-visible ~ .chm-09__bar .chm-09__burger {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger {
background: oklch(0.96 0.01 80);
color: var(--panel);
}
.chm-09__burger span {
position: relative;
width: 18px;
height: 12px;
display: block;
}
.chm-09__burger i {
position: absolute;
left: 0;
width: 18px;
height: 2px;
border-radius: 2px;
background: currentColor;
transition: translate .3s,rotate .3s .05s,opacity .2s;
}
.chm-09__burger i:nth-child(1) {
top: 0;
}
.chm-09__burger i:nth-child(2) {
top: 5px;
}
.chm-09__burger i:nth-child(3) {
top: 10px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(1) {
translate: 0 5px;
rotate: 45deg;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(2) {
opacity: 0;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(3) {
translate: 0 -5px;
rotate: -45deg;
}
.chm-09__page {
position: absolute;
inset: 0;
padding: 86px 24px 24px;
display: grid;
gap: 14px;
align-content: start;
transition: scale .5s cubic-bezier(.2,.7,.2,1),filter .5s,opacity .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__page {
scale: .96;
filter: blur(2px);
opacity: .55;
}
.chm-09__hero {
height: 190px;
border-radius: 16px;
background: linear-gradient(150deg,oklch(0.9 0.035 70),oklch(0.84 0.06 45));
display: flex;
flex-direction: column;
justify-content: end;
padding: 18px;
margin-bottom: 6px;
}
.chm-09__hero b {
font-size: 30px;
letter-spacing: -.02em;
font-family: ui-monospace,Menlo,Consolas,monospace;
}
.chm-09__hero span {
font-size: 12.5px;
color: var(--mut);
}
.chm-09__ln {
height: 11px;
width: var(--w);
border-radius: 99px;
background: oklch(0.89 0.012 80);
}
.chm-09__panel {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(170deg,color-mix(in oklch,var(--panel) 92%,white),var(--panel) 60%);
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 32px;
opacity: 0;
scale: 1.04;
visibility: hidden;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel {
opacity: 1;
scale: 1;
visibility: visible;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s 0s;
}
.chm-09__panel a {
padding: 16px 2px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: oklch(0.95 0.015 80);
text-decoration: none;
border-bottom: 1px solid oklch(1 0 0/.12);
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0;
translate: 0 14px;
transition: color .2s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}
.chm-09__panel a::after {
content: "→";
font-size: 18px;
color: var(--acc);
opacity: 0;
translate: -8px 0;
transition: opacity .2s,translate .25s;
}
.chm-09__panel a:hover::after,
.chm-09__panel a:focus-visible::after {
opacity: 1;
translate: 0 0;
}
.chm-09__panel a:hover,
.chm-09__panel a:focus-visible {
color: oklch(0.85 0.09 60);
}
.chm-09__panel a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 4px;
border-radius: 4px;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel a {
opacity: 1;
translate: 0 0;
transition-delay: 0s,calc(.14s + var(--i)*.06s),calc(.14s + var(--i)*.06s);
}
@media (prefers-reduced-motion: reduce) {
.chm-09 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}Here's a working CSS Hamburger Menu from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Pure CSS Hamburger Menu Without JavaScript
Source: https://codefronts.com/navigation/css-hamburger-menus/pure-css-hamburger-menu-no-javascript/
The checkbox hack, rebuilt for the :has() era: the input still holds the state, but :has(:checked) reads it from the ROOT, so the burger, the header, the fullscreen panel and even the page content can all react — no sibling-order gymnastics, no restructured markup. Compare demo 05 (the ~ combinator version) side by side: same zero-JS guarantee, but here the menu tints the header, scales the page and swaps the burger, all from one selector.
## HTML
```html
<section class="chm-09" aria-label="Pure CSS :has() hamburger menu demo">
<div class="chm-09__stage">
<input type="checkbox" id="chm09-t" class="chm-09__state">
<div class="chm-09__page" aria-hidden="true">
<div class="chm-09__hero"><b>:has()</b><span>state, readable anywhere</span></div>
<div class="chm-09__ln" style="--w:82%"></div>
<div class="chm-09__ln" style="--w:60%"></div>
<div class="chm-09__ln" style="--w:73%"></div>
</div>
<nav class="chm-09__panel" aria-label="Site">
<a href="#index" style="--i:0">Index</a>
<a href="#essays" style="--i:1">Essays</a>
<a href="#library" style="--i:2">Library</a>
<a href="#colophon" style="--i:3">Colophon</a>
</nav>
<header class="chm-09__bar">
<span class="chm-09__brand">marginalia</span>
<label for="chm09-t" class="chm-09__burger"><span aria-hidden="true"><i></i><i></i><i></i></span></label>
</header>
</div>
</section>
```
## CSS
```css
.chm-09,
.chm-09 *,
.chm-09 *::before,
.chm-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.chm-09 {
background: var(--bg);
width: 100%;
min-height: 100vh;
box-sizing: border-box;
--bg: oklch(0.965 0.008 85);
--ink: oklch(0.24 0.02 60);
--mut: oklch(0.52 0.02 60);
--acc: oklch(0.58 0.16 45);
--panel: oklch(0.24 0.03 55);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
display: grid;
justify-content: center;
}
.chm-09__stage {
position: relative;
width: min(400px,88cqi,88vw);
height: 540px;
border-radius: 24px;
overflow: hidden;
background: var(--bg);
border: 1px solid oklch(0.87 0.015 80);
}
.chm-09__state {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.chm-09__bar {
position: absolute;
inset: 0 0 auto 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
padding: 0 16px 0 24px;
}
.chm-09__brand {
font-size: 16px;
font-weight: 700;
font-style: italic;
letter-spacing: -.01em;
transition: color .35s;
}
.chm-09:has(#chm09-t:checked) .chm-09__brand {
color: oklch(0.95 0.015 80);
}
.chm-09__burger {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--ink);
color: oklch(0.96 0.01 80);
cursor: pointer;
transition: background .3s,scale .2s;
}
.chm-09__burger:hover {
scale: 1.06;
}
.chm-09__state:focus-visible ~ .chm-09__bar .chm-09__burger {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger {
background: oklch(0.96 0.01 80);
color: var(--panel);
}
.chm-09__burger span {
position: relative;
width: 18px;
height: 12px;
display: block;
}
.chm-09__burger i {
position: absolute;
left: 0;
width: 18px;
height: 2px;
border-radius: 2px;
background: currentColor;
transition: translate .3s,rotate .3s .05s,opacity .2s;
}
.chm-09__burger i:nth-child(1) {
top: 0;
}
.chm-09__burger i:nth-child(2) {
top: 5px;
}
.chm-09__burger i:nth-child(3) {
top: 10px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(1) {
translate: 0 5px;
rotate: 45deg;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(2) {
opacity: 0;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(3) {
translate: 0 -5px;
rotate: -45deg;
}
.chm-09__page {
position: absolute;
inset: 0;
padding: 86px 24px 24px;
display: grid;
gap: 14px;
align-content: start;
transition: scale .5s cubic-bezier(.2,.7,.2,1),filter .5s,opacity .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__page {
scale: .96;
filter: blur(2px);
opacity: .55;
}
.chm-09__hero {
height: 190px;
border-radius: 16px;
background: linear-gradient(150deg,oklch(0.9 0.035 70),oklch(0.84 0.06 45));
display: flex;
flex-direction: column;
justify-content: end;
padding: 18px;
margin-bottom: 6px;
}
.chm-09__hero b {
font-size: 30px;
letter-spacing: -.02em;
font-family: ui-monospace,Menlo,Consolas,monospace;
}
.chm-09__hero span {
font-size: 12.5px;
color: var(--mut);
}
.chm-09__ln {
height: 11px;
width: var(--w);
border-radius: 99px;
background: oklch(0.89 0.012 80);
}
.chm-09__panel {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(170deg,color-mix(in oklch,var(--panel) 92%,white),var(--panel) 60%);
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 32px;
opacity: 0;
scale: 1.04;
visibility: hidden;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel {
opacity: 1;
scale: 1;
visibility: visible;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s 0s;
}
.chm-09__panel a {
padding: 16px 2px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: oklch(0.95 0.015 80);
text-decoration: none;
border-bottom: 1px solid oklch(1 0 0/.12);
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0;
translate: 0 14px;
transition: color .2s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}
.chm-09__panel a::after {
content: "→";
font-size: 18px;
color: var(--acc);
opacity: 0;
translate: -8px 0;
transition: opacity .2s,translate .25s;
}
.chm-09__panel a:hover::after,
.chm-09__panel a:focus-visible::after {
opacity: 1;
translate: 0 0;
}
.chm-09__panel a:hover,
.chm-09__panel a:focus-visible {
color: oklch(0.85 0.09 60);
}
.chm-09__panel a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 4px;
border-radius: 4px;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel a {
opacity: 1;
translate: 0 0;
transition-delay: 0s,calc(.14s + var(--i)*.06s),calc(.14s + var(--i)*.06s);
}
@media (prefers-reduced-motion: reduce) {
.chm-09 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```Here's a working CSS Hamburger Menu from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Pure CSS Hamburger Menu Without JavaScript
Source: https://codefronts.com/navigation/css-hamburger-menus/pure-css-hamburger-menu-no-javascript/
The checkbox hack, rebuilt for the :has() era: the input still holds the state, but :has(:checked) reads it from the ROOT, so the burger, the header, the fullscreen panel and even the page content can all react — no sibling-order gymnastics, no restructured markup. Compare demo 05 (the ~ combinator version) side by side: same zero-JS guarantee, but here the menu tints the header, scales the page and swaps the burger, all from one selector.
## HTML
```html
<section class="chm-09" aria-label="Pure CSS :has() hamburger menu demo">
<div class="chm-09__stage">
<input type="checkbox" id="chm09-t" class="chm-09__state">
<div class="chm-09__page" aria-hidden="true">
<div class="chm-09__hero"><b>:has()</b><span>state, readable anywhere</span></div>
<div class="chm-09__ln" style="--w:82%"></div>
<div class="chm-09__ln" style="--w:60%"></div>
<div class="chm-09__ln" style="--w:73%"></div>
</div>
<nav class="chm-09__panel" aria-label="Site">
<a href="#index" style="--i:0">Index</a>
<a href="#essays" style="--i:1">Essays</a>
<a href="#library" style="--i:2">Library</a>
<a href="#colophon" style="--i:3">Colophon</a>
</nav>
<header class="chm-09__bar">
<span class="chm-09__brand">marginalia</span>
<label for="chm09-t" class="chm-09__burger"><span aria-hidden="true"><i></i><i></i><i></i></span></label>
</header>
</div>
</section>
```
## CSS
```css
.chm-09,
.chm-09 *,
.chm-09 *::before,
.chm-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.chm-09 {
background: var(--bg);
width: 100%;
min-height: 100vh;
box-sizing: border-box;
--bg: oklch(0.965 0.008 85);
--ink: oklch(0.24 0.02 60);
--mut: oklch(0.52 0.02 60);
--acc: oklch(0.58 0.16 45);
--panel: oklch(0.24 0.03 55);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
display: grid;
justify-content: center;
}
.chm-09__stage {
position: relative;
width: min(400px,88cqi,88vw);
height: 540px;
border-radius: 24px;
overflow: hidden;
background: var(--bg);
border: 1px solid oklch(0.87 0.015 80);
}
.chm-09__state {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.chm-09__bar {
position: absolute;
inset: 0 0 auto 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
padding: 0 16px 0 24px;
}
.chm-09__brand {
font-size: 16px;
font-weight: 700;
font-style: italic;
letter-spacing: -.01em;
transition: color .35s;
}
.chm-09:has(#chm09-t:checked) .chm-09__brand {
color: oklch(0.95 0.015 80);
}
.chm-09__burger {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--ink);
color: oklch(0.96 0.01 80);
cursor: pointer;
transition: background .3s,scale .2s;
}
.chm-09__burger:hover {
scale: 1.06;
}
.chm-09__state:focus-visible ~ .chm-09__bar .chm-09__burger {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger {
background: oklch(0.96 0.01 80);
color: var(--panel);
}
.chm-09__burger span {
position: relative;
width: 18px;
height: 12px;
display: block;
}
.chm-09__burger i {
position: absolute;
left: 0;
width: 18px;
height: 2px;
border-radius: 2px;
background: currentColor;
transition: translate .3s,rotate .3s .05s,opacity .2s;
}
.chm-09__burger i:nth-child(1) {
top: 0;
}
.chm-09__burger i:nth-child(2) {
top: 5px;
}
.chm-09__burger i:nth-child(3) {
top: 10px;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(1) {
translate: 0 5px;
rotate: 45deg;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(2) {
opacity: 0;
}
.chm-09:has(#chm09-t:checked) .chm-09__burger i:nth-child(3) {
translate: 0 -5px;
rotate: -45deg;
}
.chm-09__page {
position: absolute;
inset: 0;
padding: 86px 24px 24px;
display: grid;
gap: 14px;
align-content: start;
transition: scale .5s cubic-bezier(.2,.7,.2,1),filter .5s,opacity .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__page {
scale: .96;
filter: blur(2px);
opacity: .55;
}
.chm-09__hero {
height: 190px;
border-radius: 16px;
background: linear-gradient(150deg,oklch(0.9 0.035 70),oklch(0.84 0.06 45));
display: flex;
flex-direction: column;
justify-content: end;
padding: 18px;
margin-bottom: 6px;
}
.chm-09__hero b {
font-size: 30px;
letter-spacing: -.02em;
font-family: ui-monospace,Menlo,Consolas,monospace;
}
.chm-09__hero span {
font-size: 12.5px;
color: var(--mut);
}
.chm-09__ln {
height: 11px;
width: var(--w);
border-radius: 99px;
background: oklch(0.89 0.012 80);
}
.chm-09__panel {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(170deg,color-mix(in oklch,var(--panel) 92%,white),var(--panel) 60%);
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 32px;
opacity: 0;
scale: 1.04;
visibility: hidden;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s .5s;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel {
opacity: 1;
scale: 1;
visibility: visible;
transition: opacity .4s,scale .5s cubic-bezier(.2,.7,.2,1),visibility 0s 0s;
}
.chm-09__panel a {
padding: 16px 2px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: oklch(0.95 0.015 80);
text-decoration: none;
border-bottom: 1px solid oklch(1 0 0/.12);
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0;
translate: 0 14px;
transition: color .2s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}
.chm-09__panel a::after {
content: "→";
font-size: 18px;
color: var(--acc);
opacity: 0;
translate: -8px 0;
transition: opacity .2s,translate .25s;
}
.chm-09__panel a:hover::after,
.chm-09__panel a:focus-visible::after {
opacity: 1;
translate: 0 0;
}
.chm-09__panel a:hover,
.chm-09__panel a:focus-visible {
color: oklch(0.85 0.09 60);
}
.chm-09__panel a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 4px;
border-radius: 4px;
}
.chm-09:has(#chm09-t:checked) .chm-09__panel a {
opacity: 1;
translate: 0 0;
transition-delay: 0s,calc(.14s + var(--i)*.06s),calc(.14s + var(--i)*.06s);
}
@media (prefers-reduced-motion: reduce) {
.chm-09 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```How this works
The old hack could only style LATER siblings of the input. :has() turns the state into a root-level condition any descendant can key off:
/* 2015: state reaches siblings only */
#t:checked ~ .panel{ … }
/* 2026: state reaches EVERYTHING */
.chm-09:has(#chm09-t:checked) .panel { opacity:1; scale:1; }
.chm-09:has(#chm09-t:checked) .bar { background:transparent; }
.chm-09:has(#chm09-t:checked) .page { scale:.96; filter:blur(2px); }That third line is the tell — the page content sits in a completely different subtree from the input, and it still responds. This is what previously forced people into JavaScript.
The panel here is a fullscreen fade-and-settle (opacity + scale:1.04→1 + delayed visibility) rather than a slide, to demonstrate a second point: with the state readable anywhere, your open animation is unconstrained by where the panel lives in the DOM. Links stagger via transition-delay: calc(var(--i)*.06s), and the label's focus ring is re-projected from the hidden input exactly as in demo 05 — that discipline doesn't change. Same honest limits as every no-JS menu: announces as a checkbox, no Escape, no focus management. What you gain over demo 05 is architectural freedom; what you still don't get is ARIA truth. Choose accordingly.
Make it yours
- Page-behind treatment is one rule —
scale:.96; filter:blur(2px). Delete it for a plain overlay, or push tograyscale(1)for editorial drama. - Panel motion: swap
scale:1.04fortranslate:0 20pxfor a rise-in; the :has() wiring is animation-agnostic. - Because state is root-readable, you can add an 'open' progress bar in the header, tint the brand, swap logo color — anything, one
.chm-09:has(:checked)prefix away. - Multiple independent menus on one page: keep each input id unique and scope each :has() to its own component root, exactly as done here.
Gotchas — read before shipping
- :has() invalidation is fast but not free — keep the :has() conditions on a component root, not on
htmlorbody, so the browser re-checks a small subtree per toggle. - This pattern still can't close on Escape or return focus — those are JS behaviors. If your audit requires them (most app-nav audits do), graduate to demo 02/17.
- Firefox shipped :has() in 121 (Dec 2023) — for long-tail enterprise Firefox ESR installs, demo 05's sibling version is the safe fallback; ship both behind @supports(selector(:has(*))) if you must.
- Keep the input focusable (clip pattern, never display:none) — Space is the ONLY keyboard affordance a no-JS menu has.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 121+ | 111+ |
:has() — Chrome 105, Safari 15.4, Firefox 121. Everything else here is decade-old CSS; wrap the enhancements in @supports(selector(:has(*))) and fall back to demo 05's engine for older Firefox.