Tabs
Horizontal tab strip with an underline indicator. Two recipes share the .tabs/.tab skin: a plain link strip (<ul class="tabs"><li><a class="tab active">) where YOU wire the switching, and the zero-JS .tabset subcomponent that switches panes with hidden radio inputs and :has().
Root: .tabs on <ul>, <div>, <nav>
Example
Contract
Structure
| Selector | Required | Description |
|---|---|---|
.tab | required | The tab items. In the link recipe: <li><a class="tab"> with .active on the current one (.disabled to disable). In the tabset recipe: <label class="tab"> immediately following its radio input. |
Accessibility
The link recipe carries no tab semantics by itself — add role="tablist"/"tab"/"tabpanel" and aria-selected if you script it. The .tabset recipe rides on radio semantics instead: focus the group and switch with arrow keys; do not also add ARIA tab roles on top of the radios.
Nesting
Allowed in:
detail viewscards.tabset (as the strip of a CSS-only tabset)
Never inside (or containing):
.tabs
Subcomponents
Tabset
CSS-only switching tabs: hidden radio inputs inside .tabs drive which .tab-pane is visible via :has() — no JavaScript. Supports at most 8 tabs (the :has()/:nth pane-matching rules are generated for indices 1-8).
Root: .tabset on <div>
Example
<div class="tabset"><div class="tabs"><input type="radio" name="t" id="t1" checked><label class="tab" for="t1">Details</label><input type="radio" name="t" id="t2"><label class="tab" for="t2">Comments</label></div><div class="tab-panes"><div class="tab-pane">Details…</div><div class="tab-pane">Comments…</div></div></div>
Structure
| Selector | Required | Description |
|---|---|---|
.tabs | required | The strip. Its children must be alternating pairs: <input type="radio"> immediately followed by its <label class="tab"> (the CSS selects input:checked + .tab). All radios share one name; each label's for matches its input's id; exactly one input carries checked. |
.tabs > input[type=radio] | required | Visually-hidden radios as DIRECT children of .tabs. CRITICAL ordering constraint: the Nth radio (input:nth-of-type(N)) shows the Nth pane (.tab-pane:nth-child(N)) — radio order and pane order must correspond exactly. |
.tabs > label.tab | required | Each label must be the immediate next sibling of its radio, and its for attribute must reference that radio's id. |
.tab-panes | required | Wrapper whose element children are ONLY .tab-pane divs, in the same order as the radios (pane visibility is matched by :nth-child, so any extra child element breaks the correspondence). |
.tab-panes > .tab-pane | required | One pane per tab, direct children of .tab-panes, same count and order as the radios. |
Accessibility
Keyboard support comes from native radio groups (arrow keys switch, which also switches panes); the focused radio shows a focus ring on its label. Give the radio group a unique name per tabset on the page. Panes are display:none when inactive, so hidden content is correctly removed from the accessibility tree.
Nesting
Allowed in:
detail viewscards.demo-main / main content
Never inside (or containing):
.tabset