Underline Tabs
A keyboard-accessible tab strip with the active tab underlined.
6 frameworksIntermediate
Tabs that split the row into equal columns to fill their container.
<!-- Equal-width tabs: each tab is flex-1 with min-w-0/truncate, so the strip fills the row and long labels shorten instead of overflowing. -->
<div class="max-w-2xl" data-tabs-fullwidth>
<div class="flex border-b border-gray-200 dark:border-gray-800" role="tablist" aria-label="Account tabs">
<button class="-mb-px min-w-0 flex-1 truncate border-b-2 border-transparent px-2 py-2.5 text-center text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:border-blue-600 aria-selected:text-blue-600 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:border-blue-400 dark:aria-selected:text-blue-400" type="button" role="tab" id="fw-summary" aria-controls="fw-panel-summary" aria-selected="true" tabindex="0">Summary</button>
<button class="-mb-px min-w-0 flex-1 truncate border-b-2 border-transparent px-2 py-2.5 text-center text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:border-blue-600 aria-selected:text-blue-600 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:border-blue-400 dark:aria-selected:text-blue-400" type="button" role="tab" id="fw-activity" aria-controls="fw-panel-activity" aria-selected="false" tabindex="-1">Activity</button>
<button class="-mb-px min-w-0 flex-1 truncate border-b-2 border-transparent px-2 py-2.5 text-center text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 motion-reduce:transition-none aria-selected:border-blue-600 aria-selected:text-blue-600 dark:text-gray-400 dark:hover:text-gray-100 dark:focus-visible:ring-blue-400 dark:aria-selected:border-blue-400 dark:aria-selected:text-blue-400" type="button" role="tab" id="fw-notes" aria-controls="fw-panel-notes" aria-selected="false" tabindex="-1">Notes</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="fw-panel-summary" aria-labelledby="fw-summary" tabindex="0"><p>Key numbers for the account at a glance.</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="fw-panel-activity" aria-labelledby="fw-activity" tabindex="0" hidden><p>The most recent events on this account.</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="fw-panel-notes" aria-labelledby="fw-notes" tabindex="0" hidden><p>Internal notes shared with your team.</p></div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs-fullwidth]').forEach(function (root) {
var tabs = [].slice.call(root.querySelectorAll('[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 | Type | Default | Description |
|---|---|---|---|
itemsrequired | TabItem[] | - | The array of items to render. |
defaultTabId | string | - | Id of the item expanded on mount. |
onSelect | (id: string) => void | - | Fired with the menu item the user chose. |
className | string | - | Additional classes merged onto the root element. |
Each tab is `flex-1` with `min-w-0` and `truncate`, so the strip fills the width and long labels shorten instead of forcing an overflow - the layout of choice for a mobile bottom bar. Cap the tab count to what fits legibly; past four or five, reach for the scrollable or overflow variant.