Interactive component
Tabs
Zero-JS tabs built on native <input type="radio"> + :has() + :nth-of-type(). The browser tracks which tab is active; CSS shows the matching panel. No state machine, no React, no JSON of stories.
0 KB JS for core:has() Baseline Dec 2023Up to 12 tabs default
Overview content — this demo is the real
cia.tabs output on real radios. Zero JavaScript: the browser tracks the checked radio, CSS shows the matching panel.Specs content — the tab list is a
<nav> on purpose: panel switching counts panels with :nth-of-type(), so the list must not share their element type.Reviews content.
Usage
// app.scss @use 'css-is-awesome' as cia; .product-tabs { @include cia.tabs; }
<!-- index.html --> <div class="product-tabs"> <input type="radio" name="t" id="t1" checked> <input type="radio" name="t" id="t2"> <input type="radio" name="t" id="t3"> <nav class="cia-tab-list" role="tablist"> <label for="t1" role="tab">Overview</label> <label for="t2" role="tab">Specs</label> <label for="t3" role="tab">Reviews</label> </nav> <div class="cia-tab-panel" role="tabpanel">Overview content</div> <div class="cia-tab-panel" role="tabpanel">Specs content</div> <div class="cia-tab-panel" role="tabpanel">Reviews content</div> </div>
| Metric | cia | Tailwind + Headless UI |
|---|---|---|
| JavaScript shipped | 0 KB for core | ~8 KB (Tab.Group) |
| Active-panel switch | :has(input:nth-of-type(N):checked) | React state |
| Keyboard nav | Native radio (Tab + Arrows + Space) | Wired in component |
| SSR | Works as-is, initial tab via checked | Hydrates after render |
| aria-selected sync | Opt-in JS shim (~200 bytes) | Wired in component |