Underline Tabs
A keyboard-accessible tab strip with the active tab underlined.
6 फ्रेमवर्कमध्यम
Editor-style tabs each with a real close button you can dismiss.
<!--
Closable tabs. The close control is a real sibling <button> with its own
aria-label - NOT a glyph inside the tab (nested buttons are invalid, and an
icon inside the tab is not separately operable). The script removes the tab and
moves selection to a neighbour when the active tab is closed.
-->
<div class="max-w-2xl" data-tabs-closable>
<div class="flex gap-1 overflow-x-auto border-b border-gray-200 dark:border-gray-800" role="tablist" aria-label="Open files">
<span class="inline-flex shrink-0 items-center">
<button class="-mb-px whitespace-nowrap rounded-t-md border-b-2 border-transparent py-2 pl-3 pr-1.5 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="cl-index" aria-controls="cl-panel-index" aria-selected="true" tabindex="0">index.ts</button>
<button class="-mb-px flex h-6 w-6 flex-none items-center justify-center rounded text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus-visible:ring-blue-400" type="button" data-close aria-label="Close index.ts">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="h-3.5 w-3.5" aria-hidden="true"><path d="M5 5l10 10M15 5 5 15" stroke-linecap="round" /></svg>
</button>
</span>
<span class="inline-flex shrink-0 items-center">
<button class="-mb-px whitespace-nowrap rounded-t-md border-b-2 border-transparent py-2 pl-3 pr-1.5 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="cl-app" aria-controls="cl-panel-app" aria-selected="false" tabindex="-1">app.tsx</button>
<button class="-mb-px flex h-6 w-6 flex-none items-center justify-center rounded text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 motion-reduce:transition-none dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus-visible:ring-blue-400" type="button" data-close aria-label="Close app.tsx">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="h-3.5 w-3.5" aria-hidden="true"><path d="M5 5l10 10M15 5 5 15" stroke-linecap="round" /></svg>
</button>
</span>
</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="cl-panel-index" aria-labelledby="cl-index" tabindex="0"><p>The entry point that wires the app together.</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="cl-panel-app" aria-labelledby="cl-app" tabindex="0" hidden><p>The root component and its providers.</p></div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs-closable]').forEach(function (root) {
function tabs() { return [].slice.call(root.querySelectorAll('[role="tab"]')); }
function panelFor(t) { return document.getElementById(t.getAttribute('aria-controls')); }
function select(tab, focus) {
tabs().forEach(function (t) {
var on = t === tab;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
var p = panelFor(t);
if (p) p.hidden = !on;
});
if (focus && tab) tab.focus();
}
root.addEventListener('click', function (e) {
var tab = e.target.closest('[role="tab"]');
if (tab) { select(tab, false); return; }
var close = e.target.closest('[data-close]');
if (!close) return;
var group = close.closest('span');
var owned = group.querySelector('[role="tab"]');
var list = tabs();
var idx = list.indexOf(owned);
var wasActive = owned.getAttribute('aria-selected') === 'true';
var panel = panelFor(owned);
if (panel) panel.remove();
group.remove();
if (wasActive) {
var next = tabs();
var fallback = next[idx] || next[idx - 1] || next[0];
if (fallback) select(fallback, true);
}
});
root.addEventListener('keydown', function (e) {
var tab = e.target.closest('[role="tab"]');
if (!tab) return;
var list = tabs();
var i = list.indexOf(tab);
var n = -1;
if (e.key === 'ArrowRight') n = (i + 1) % list.length;
else if (e.key === 'ArrowLeft') n = (i - 1 + list.length) % list.length;
else if (e.key === 'Home') n = 0;
else if (e.key === 'End') n = list.length - 1;
else return;
e.preventDefault();
select(list[n], true);
});
});
})();
</script>| Prop | टाइप | डिफ़ॉल्ट | विवरण |
|---|---|---|---|
itemsआवश्यक | TabItem[] | - | रेंडर की जाने वाली आइटम की सूची। |
defaultTabId | string | - | माउंट पर खुले रहने वाले आइटम की id। |
onClose | (id: string) => void | - | यूज़र द्वारा चुने गए मेन्यू आइटम के साथ चलता है। |
className | string | - | रूट एलिमेंट पर मर्ज होने वाली अतिरिक्त क्लासेज़। |
The close control is a real sibling `<button>` with its own `aria-label`, not a glyph baked into the tab - nested buttons are invalid and an icon inside the tab is not separately operable. Closing the active tab moves selection (and focus) to a neighbour so the keyboard user is never stranded.