A classic tab strip with one centralized handler on the parent. The `take` command moves the active state from sibling tabs to the clicked one in a single statement.
A classic tab strip - click a tab, that tab becomes active and its panel
shows. One handler on the parent uses event delegation to catch any tab
click, then take moves the aria-selected state to the
clicked tab and add ... when toggles the hidden attribute on the panels.
<div class="tabs" _="on click
set tab to the closest <[role=tab]/> to the target
if no tab exit end
take @aria-selected='true' from <[role=tab]/> in me giving 'false' for tab
add @hidden to <[role=tabpanel]/> in me when its @id is not (tab's @aria-controls)">
<div class="tab-bar" role="tablist">
<button class="tab" role="tab" aria-selected="true" aria-controls="panel-overview">Overview</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="panel-specs">Specs</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="panel-reviews">Reviews</button>
</div>
<div class="panel" id="panel-overview" role="tabpanel">
<p>A medium-roast coffee with notes of caramel, dried fruit, and dark
chocolate. Sourced from a small cooperative in Huila, Colombia.</p>
</div>
<div class="panel" id="panel-specs" role="tabpanel" hidden>
<ul>
<li>Origin: Huila, Colombia</li>
<li>Roast: Medium</li>
<li>Process: Washed</li>
<li>Altitude: 1,650-1,950m</li>
</ul>
</div>
<div class="panel" id="panel-reviews" role="tabpanel" hidden>
<p><strong>★★★★★</strong> "My new daily driver." - Jamie</p>
<p><strong>★★★★☆</strong> "Bright, clean, lovely finish." - Riley</p>
</div>
</div>
A medium-roast coffee with notes of caramel, dried fruit, and dark chocolate. Sourced from a small cooperative in Huila, Colombia.
★★★★★ "My new daily driver." - Jamie
★★★★☆ "Bright, clean, lovely finish." - Riley
The whole tab strip is one handler on the parent:
on click
set tab to the closest <[role=tab]/> to the target
if no tab exit end
take @aria-selected='true' from <[role=tab]/> in me giving 'false' for tab
add @hidden to <[role=tabpanel]/> in me when its @id is not (tab's @aria-controls)
Four lines, in order:
set tab to the closest <[role=tab]/> to the target - find the tab the user clicked. the target is the click event's event.target, and closest walks up to find the nearest ancestor (or self) matching the selector. If they clicked outside any tab, tab is null.
if no tab exit end - bail out cleanly when the click wasn't on a tab.
take @aria-selected='true' from <[role=tab]/> in me giving 'false' for tab - the take shibboleth, in its richest form:
@aria-selected='true' is what gets added to the for targetfrom <[role=tab]/> in me is the source set, scoped to descendants of this tablistgiving 'false' is what the from elements get instead of having the attribute removed (giving is an alias for with, and reads more naturally in this position)for tab is the destinationNet effect: every tab in this component gets aria-selected="false", then the clicked tab gets aria-selected="true". The browser styles it via [aria-selected="true"] in CSS - no .active class needed.
add @hidden to <[role=tabpanel]/> in me when its @id is not (tab's @aria-controls) - for each tabpanel, add @hidden if its id doesn't match the active tab's aria-controls, and remove it if it does. add ... when toggles in both directions, so one statement handles all panels at once.
.activerole="tablist" / role="tab" / role="tabpanel" tell screen readers
this is a tab strip, not just a row of buttons. Users get keyboard
navigation hints and the right reading order.aria-selected="true" is the announced state. CSS can target it
directly with [aria-selected="true"], so the visual state and the
accessibility state are the same attribute - they can never drift.aria-controls="panel-id" wires each tab to its panel without
needing a separate data attribute. The same string serves the screen
reader and our add ... when predicate.hidden attribute does what we want for free: it
applies display: none via the UA stylesheet, AND it makes the panel
inert and unannounced. No aria-hidden needed.This is the rare case where the accessible markup is also the shorter markup: one handler, two statements, zero JS classes to manage.
take shines hereThe vanilla equivalent of just the active-tab toggle is six lines:
document.querySelectorAll('[role=tab]').forEach(t => t.setAttribute('aria-selected', 'false'));
clickedTab.setAttribute('aria-selected', 'true');
document.querySelectorAll('[role=tabpanel]').forEach(p => p.hidden = true);
document.getElementById(clickedTab.getAttribute('aria-controls')).hidden = false;
Hyperscript collapses each forEach + set into one statement because
that's the operation take was named for: take this attribute (with
this value) away from a group, give it to one element. Tabs, segmented
controls, "selected row" highlights, mode switches, breadcrumb-style
step indicators - any mutually-exclusive UI state is a take away.
Heads up: the component version of this pattern lives at Tab Set Component, which wraps the same logic into a
<tab-set>custom element.