Pill Tabs
A segmented control where the selected tab becomes a filled pill.
6 個のフレームワーク初級
A keyboard-accessible tab strip with the active tab underlined.
<!--
The ARIA tabs pattern, in full:
- the strip is role="tablist", each control role="tab", each panel role="tabpanel"
- aria-controls points a tab at its panel; aria-labelledby points back
- ROVING TABINDEX: the selected tab has tabindex="0", the rest tabindex="-1",
so Tab lands on the strip once and then moves on to the panel
- the panel takes tabindex="0" so it is reachable when it holds no controls
Arrow/Home/End handling lives in the script below - without it this is just a
row of buttons wearing tab roles.
-->
<div class="tabs" data-tabs>
<div class="tabs__list" role="tablist" aria-label="Account settings">
<button class="tabs__tab" type="button" role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0">
Overview
</button>
<button class="tabs__tab" type="button" role="tab" id="tab-activity" aria-controls="panel-activity" aria-selected="false" tabindex="-1">
Activity
</button>
<button class="tabs__tab" type="button" role="tab" id="tab-settings" aria-controls="panel-settings" aria-selected="false" tabindex="-1">
Settings
</button>
</div>
<div class="tabs__panel" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">
<p>Your workspace is on the Team plan with five of ten seats in use.</p>
</div>
<div class="tabs__panel" role="tabpanel" id="panel-activity" aria-labelledby="tab-activity" tabindex="0" hidden>
<p>Nine deployments shipped this week, the last one four minutes ago.</p>
</div>
<div class="tabs__panel" role="tabpanel" id="panel-settings" aria-labelledby="tab-settings" tabindex="0" hidden>
<p>Two-factor authentication is enforced for every member of this workspace.</p>
</div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs]').forEach(function (root) {
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
var panels = Array.prototype.slice.call(root.querySelectorAll('[role="tabpanel"]'));
function select(index, setFocus) {
tabs.forEach(function (tab, i) {
var isSelected = i === index;
tab.setAttribute('aria-selected', String(isSelected));
// Roving tabindex: exactly one tab is ever in the tab sequence.
tab.tabIndex = isSelected ? 0 : -1;
if (panels[i]) panels[i].hidden = !isSelected;
});
if (setFocus) tabs[index].focus();
}
tabs.forEach(function (tab, i) {
tab.addEventListener('click', function () {
select(i, false);
});
tab.addEventListener('keydown', function (event) {
var next = -1;
if (event.key === 'ArrowRight') next = (i + 1) % tabs.length;
else if (event.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = tabs.length - 1;
else return;
event.preventDefault();
select(next, true);
});
});
});
})();
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
items必須 | TabItem[] | - | レンダリングする項目の配列。 |
defaultTabId | string | - | マウント時に展開する項目の ID。 |
onSelect | (id: string) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
className | string | - | ルート要素にマージされる追加クラス。 |
The indicator is an `::after` pseudo-element pinned to the list's hairline, not a `border-bottom` on the tab - a border would nudge the label by a pixel as it appeared. Change the accent in the two `aria-selected` rules and nowhere else; the state lives in the ARIA attribute, so the paint and the screen-reader announcement can never drift apart. Keep the roving tabindex intact: exactly one tab is `tabIndex={0}`, which is what lets Tab skip the strip and land on the panel rather than walking through every tab.