Practical, copy‑paste CSS state system patterns (no JS) built on hidden‑checkbox/label toggles, :has(), style queries, and other modern CSS features:
1) Hidden‑Checkbox Toggle (classic “CSS checkbox hack”)
Use a hidden <input type="checkbox"> as a state driver. Clicking the <label> toggles :checked which styles siblings.
Works for menus, accordions, modals, etc.(CSS-Tricks)
<input type="checkbox" id="toggle" hidden>
<label for="toggle">Menu</label>
<nav class="menu">
<a href="#">Home</a>
<a href="#">About</a>
</nav>
<style>
.menu { display: none; }
#toggle:checked + label + .menu {
display: block;
}
</style>Pattern:
#stateElement:checked + label + .target { /* active style */ }
Use <details> + <summary> — built‑in toggle with no CSS/JS. You can style based on the open attribute.(web.dev)
<details>
<summary>Section 1</summary>
<p>Hidden content</p>
</details>
<style>
details[open] summary { font-weight: bold; }
</style>Pattern:
details[open] .child { /* open styles */ }
Use :has() to style a parent based on a child state (e.g., open menus/tabs) — ideal for complex panel toggles.(Medium)
<nav class="nav">
<button class="btn">Toggle</button>
<ul class="links">
<li>Item</li>
</ul>
</nav>
<style>
.nav:has(.btn:focus + .links) {
background: rgba(0,0,0,.1);
}
</style>Pattern:
.container:has(.trigger:active) .target { /* active styles */ }
Use CSS style queries (@when, @else) to adapt UI based on container dimension — can replace many JS layout checks.
Note: relatively new / experimental spec (
@whenetc. is evolving).(YouTube)
@container (min-width: 400px) {
.card { padding: 2rem; }
}Pattern:
@container (condition) { selector { styles } }
5) Pure CSS Tabs (hidden radios + label)
Radios ensure mutually exclusive states (tabs) without JS.(CSS-Tricks)
<input type="radio" name="tab" id="tab1" hidden checked>
<label for="tab1">Tab 1</label>
<input type="radio" name="tab" id="tab2" hidden>
<label for="tab2">Tab 2</label>
<section id="content1">…</section>
<section id="content2">…</section>
<style>
#tab1:checked ~ #content1 { display: block; }
#tab2:checked ~ #content2 { display: block; }
section { display: none; }
</style>If using custom elements with a CustomStateSet, :state() lets CSS react to custom states — useful for web components.(MDN Web Docs)
my-toggle:state(on) { background: lime; }
my-toggle:state(off) { background: grey; }Checkbox state can trigger CSS transitions/animations — no JS timers needed.(CSS-Tricks)
#toggle:checked ~ .box {
transform: translateX(100px);
transition: transform .3s ease;
}| Pattern | CSS Trigger | Use Cases |
|---|---|---|
| Hidden checkbox | :checked |
Menus, modals, overlays |
<details> |
open attribute |
Disclosure/accordion |
:has() parent selector |
child state | Tabs, dropdowns |
| Style queries | @container |
Responsive UI logic |
| Radio tabs | :checked excl. |
Tab systems |
| Custom states | :state() |
Web components |