Underline Tabs
A keyboard-accessible tab strip with the active tab underlined.
6 个框架中级
A tab strip whose tail collapses into a "More" dropdown menu.
<!--
Overflow tabs. Tabs past a threshold fold into a "More" menu instead of
scrolling. The trigger reports aria-haspopup="menu" and aria-expanded, lights up
when the active tab is hidden inside it, and the menu closes on Escape, outside
click or selection.
-->
<div class="max-w-2xl" data-tabs-overflow>
<div class="flex items-stretch gap-6 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="ov-general" aria-controls="ov-panel-general" aria-selected="true" tabindex="0">General</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="ov-members" aria-controls="ov-panel-members" aria-selected="false" tabindex="-1">Members</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="ov-billing" aria-controls="ov-panel-billing" aria-selected="false" tabindex="-1">Billing</button>
<div class="relative flex items-stretch" data-overflow-wrap>
<button class="relative inline-flex items-center gap-1 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 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400" type="button" data-more aria-haspopup="menu" aria-expanded="false">
More
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" class="h-4 w-4" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
<div class="absolute right-0 top-full z-10 mt-1 hidden min-w-40 rounded-lg border border-gray-200 bg-white p-1 shadow-lg dark:border-gray-800 dark:bg-gray-900" role="menu" aria-label="More tabs" data-menu>
<button class="block w-full rounded-md px-3 py-1.5 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 motion-reduce:transition-none dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:ring-blue-400" type="button" role="menuitem" data-target="ov-webhooks">Webhooks</button>
<button class="block w-full rounded-md px-3 py-1.5 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 motion-reduce:transition-none dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:ring-blue-400" type="button" role="menuitem" data-target="ov-audit">Audit log</button>
</div>
</div>
</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="ov-panel-general" aria-labelledby="ov-general" tabindex="0"><p>Workspace name, logo and default language.</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="ov-panel-members" aria-labelledby="ov-members" tabindex="0" hidden><p>24 members across 5 teams.</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="ov-panel-billing" aria-labelledby="ov-billing" tabindex="0" hidden><p>Team plan, renews on the 1st.</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="ov-panel-webhooks" aria-labelledby="ov-webhooks" tabindex="0" hidden><p>Two endpoints, both responding with 200.</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="ov-panel-audit" aria-labelledby="ov-audit" tabindex="0" hidden><p>Every privileged action, retained for a year.</p></div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs-overflow]').forEach(function (root) {
var tabs = [].slice.call(root.querySelectorAll('[role="tab"]'));
var wrap = root.querySelector('[data-overflow-wrap]');
var more = root.querySelector('[data-more]');
var menu = root.querySelector('[data-menu]');
var menuItems = [].slice.call(root.querySelectorAll('[role="menuitem"]'));
function panelById(id) { return document.getElementById('ov-panel-' + id.replace('ov-', '')); }
function panelFor(t) { return document.getElementById(t.getAttribute('aria-controls')); }
var overflowIds = menuItems.map(function (m) { return m.getAttribute('data-target'); });
function selectId(id, focusTab) {
tabs.concat([]).forEach(function () {});
// hide every panel, then show the target's
root.querySelectorAll('[role="tabpanel"]').forEach(function (p) { p.hidden = p.id !== 'ov-panel-' + id.replace('ov-', ''); });
tabs.forEach(function (t) {
var on = t.id === id;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
});
var inOverflow = overflowIds.indexOf(id) !== -1;
if (inOverflow) {
more.setAttribute('aria-selected', 'true');
more.classList.add('text-blue-600', 'dark:text-blue-400');
if (tabs[0]) tabs[0].tabIndex = 0; // keep the strip reachable by Tab
} else {
more.removeAttribute('aria-selected');
more.classList.remove('text-blue-600', 'dark:text-blue-400');
}
var owner = document.getElementById(id);
if (focusTab && owner) owner.focus();
}
function closeMenu() { menu.classList.add('hidden'); more.setAttribute('aria-expanded', 'false'); }
function openMenu() { menu.classList.remove('hidden'); more.setAttribute('aria-expanded', 'true'); }
tabs.forEach(function (t) {
t.addEventListener('click', function () { selectId(t.id, false); });
t.addEventListener('keydown', function (e) {
var i = tabs.indexOf(t);
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();
selectId(tabs[n].id, true);
});
});
more.addEventListener('click', function () {
if (menu.classList.contains('hidden')) openMenu(); else closeMenu();
});
more.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); });
menuItems.forEach(function (m) {
m.addEventListener('click', function () {
selectId(m.getAttribute('data-target'), false);
closeMenu();
more.focus();
});
});
document.addEventListener('mousedown', function (e) {
if (!wrap.contains(e.target)) closeMenu();
});
});
})();
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
items必填 | TabItem[] | - | 要渲染的项目数组。 |
maxVisible | number | 3 | 合并到根元素上的额外类名。 |
defaultTabId | string | - | 挂载时默认展开项的 id。 |
className | string | - | 合并到根元素上的额外类名。 |
Tabs past `maxVisible` fold into a `More` menu instead of scrolling; the trigger reports `aria-haspopup="menu"` and `aria-expanded`, lights up when the active tab is hidden inside it, and the menu closes on Escape, outside click, or selection. Keep the first visible tab in the tab sequence when the active tab is in the menu so keyboard users still reach the strip.