CSS-Only Tabs & Toggles
When to Use
Use radio inputs +
:checked+ sibling selectors for CSS-only tab interfaces. Add ARIA roles for accessibility. Switch to JavaScript when you have more than 5 panels.
Decision
| Client asks for... | Use... | Why |
|---|---|---|
| Tab interface | Radio inputs + :checked + sibling selectors |
CSS-only state management |
| Toggle panel (show/hide) | <details> or checkbox + :checked |
Built-in toggle |
| Segmented control | Radio inputs styled as buttons | Same pattern as tabs |
| Content switcher (A/B) | Checkbox :checked + sibling selectors |
Binary toggle |
| Dark mode toggle | Checkbox :checked + :has() on html |
Parent-based state |
Pattern
<div class="tabs">
<input type="radio" name="tab" id="tab1" checked class="tabs__input">
<label for="tab1" class="tabs__label">Tab 1</label>
<input type="radio" name="tab" id="tab2" class="tabs__input">
<label for="tab2" class="tabs__label">Tab 2</label>
<div class="tabs__panel" id="panel1">Content 1</div>
<div class="tabs__panel" id="panel2">Content 2</div>
</div>
.tabs__input { position: absolute; opacity: 0; pointer-events: none; }
.tabs__label {
display: inline-block; padding: 0.75rem 1.5rem;
cursor: pointer; border-bottom: 2px solid transparent;
transition: border-color 0.2s, color 0.2s;
}
.tabs__input:checked + .tabs__label {
border-bottom-color: var(--color-primary); color: var(--color-primary);
}
.tabs__input:focus-visible + .tabs__label {
outline: 2px solid var(--color-primary); outline-offset: 2px;
}
.tabs__panel { display: none; }
#tab1:checked ~ #panel1,
#tab2:checked ~ #panel2 { display: block; }
/* Dark mode toggle with :has() */
html:has(#dark-toggle:checked) {
color-scheme: dark;
--bg: oklch(15% 0 0);
--text: oklch(90% 0 0);
}
Accessibility: Add role="tablist" on container, role="tab" on labels, role="tabpanel" on panels. Toggle switches need role="switch" and aria-checked.
Common Mistakes
- Using
display: noneon radio inputs — breaks keyboard navigation; useopacity: 0with positioning - Missing focus styles — hidden radio inputs need
:focus-visible + labelstyling - Not adding ARIA roles — CSS-only tabs need ARIA attributes for screen readers
- Too many tabs — CSS-only tabs with >5 panels get unwieldy; use JS at that point
See Also
- CSS-Only Accordions → vertical show/hide
- CSS-Only Popovers → click-triggered panels
- Modern CSS Craft Patterns →
@starting-stylepanel entry animations