Underline Tabs
A keyboard-accessible tab strip with the active tab underlined.
6 个框架中级
Tabs inside a tab panel, each level with its own keyboard scope.
<!--
Nested tabs. The correctness point is scope: the script keys each tablist to
its OWN tabs and finds panels by aria-controls, so the inner strip does not
answer the outer strip's arrow keys and each level runs its own roving tabindex.
-->
<div class="max-w-2xl" data-tabs-nested>
<div class="flex gap-6 overflow-x-auto border-b border-gray-200 dark:border-gray-800" role="tablist" aria-label="Settings">
<button class="relative whitespace-nowrap px-0.5 py-3 text-sm font-medium text-gray-600 transition-colors after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:bg-transparent hover:text-gray-900 focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:text-blue-600 aria-selected:after:bg-blue-600 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:text-blue-400 dark:aria-selected:after:bg-blue-400" type="button" role="tab" id="ns-account" aria-controls="ns-panel-account" aria-selected="true" tabindex="0">Account</button>
<button class="relative whitespace-nowrap px-0.5 py-3 text-sm font-medium text-gray-600 transition-colors after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:bg-transparent hover:text-gray-900 focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:text-blue-600 aria-selected:after:bg-blue-600 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:text-blue-400 dark:aria-selected:after:bg-blue-400" type="button" role="tab" id="ns-workspace" aria-controls="ns-panel-workspace" aria-selected="false" tabindex="-1">Workspace</button>
</div>
<div class="pt-4 text-sm leading-relaxed text-gray-600 focus-visible:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:text-gray-300 dark:focus-visible:ring-blue-400" role="tabpanel" id="ns-panel-account" aria-labelledby="ns-account" tabindex="0"><p>Your profile, email and password live here.</p></div>
<div class="pt-4 text-sm leading-relaxed text-gray-600 focus-visible:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:text-gray-300 dark:focus-visible:ring-blue-400" role="tabpanel" id="ns-panel-workspace" aria-labelledby="ns-workspace" tabindex="0" hidden>
<p class="mb-3">Manage how billing works for the whole workspace.</p>
<div class="inline-flex max-w-full gap-1 overflow-x-auto rounded-full bg-gray-100 p-1 dark:bg-gray-800" role="tablist" aria-label="Billing sections">
<button class="whitespace-nowrap rounded-full px-3 py-1 text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:bg-blue-600 aria-selected:text-white dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:text-white" type="button" role="tab" id="ns-plan" aria-controls="ns-panel-plan" aria-selected="true" tabindex="0">Plan</button>
<button class="whitespace-nowrap rounded-full px-3 py-1 text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:bg-blue-600 aria-selected:text-white dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:text-white" type="button" role="tab" id="ns-invoices" aria-controls="ns-panel-invoices" aria-selected="false" tabindex="-1">Invoices</button>
</div>
<div class="pt-3 text-sm leading-relaxed text-gray-600 focus-visible:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:text-gray-300 dark:focus-visible:ring-blue-400" role="tabpanel" id="ns-panel-plan" aria-labelledby="ns-plan" tabindex="0"><p>Team plan, 24 seats, renews monthly.</p></div>
<div class="pt-3 text-sm leading-relaxed text-gray-600 focus-visible:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:text-gray-300 dark:focus-visible:ring-blue-400" role="tabpanel" id="ns-panel-invoices" aria-labelledby="ns-invoices" tabindex="0" hidden><p>12 paid invoices, all downloadable as PDF.</p></div>
</div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs-nested] [role="tablist"]').forEach(function (list) {
// :scope keeps each tablist to its own tabs, so nesting stays isolated.
var tabs = [].slice.call(list.querySelectorAll(':scope > [role="tab"]'));
function panelFor(t) { return document.getElementById(t.getAttribute('aria-controls')); }
function select(i, focus) {
tabs.forEach(function (t, n) {
var on = n === i;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
var p = panelFor(t);
if (p) p.hidden = !on;
});
if (focus) tabs[i].focus();
}
tabs.forEach(function (t, i) {
t.addEventListener('click', function () { select(i, false); });
t.addEventListener('keydown', function (e) {
var n = -1;
if (e.key === 'ArrowRight') n = (i + 1) % tabs.length;
else if (e.key === 'ArrowLeft') n = (i - 1 + tabs.length) % tabs.length;
else if (e.key === 'Home') n = 0;
else if (e.key === 'End') n = tabs.length - 1;
else return;
e.preventDefault();
select(n, true);
});
});
});
})();
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
items必填 | TabItem[] | - | 要渲染的项目数组。 |
variant | 'underline' | 'pills' | - | 合并到根元素上的额外类名。 |
defaultTabId | string | - | 挂载时默认展开项的 id。 |
className | string | - | 合并到根元素上的额外类名。 |
One self-contained `Tabs` component reused at both levels is what keeps the inner strip from answering the outer strip's arrow keys - each `tablist` runs its own roving tabindex. Style the inner level differently (pills under an underline) so the hierarchy reads at a glance, and keep nesting to two levels before a user loses the plot.