Underline Tabs
A keyboard-accessible tab strip with the active tab underlined.
6 个框架中级
Folder-style tabs where the active tab joins the panel below it.
<!--
Boxed (folder) tabs. The trick is the seam: the tablist has a bottom border,
the selected tab pulls itself down 1px (margin-bottom: -1px) and paints its own
background over that border, so the tab and the panel below read as one sheet
of paper. Everything else is the same ARIA + roving tabindex machinery.
-->
<div class="tabs-boxed" data-tabs-boxed>
<div class="tabs-boxed__list" role="tablist" aria-label="Editor view">
<button class="tabs-boxed__tab" type="button" role="tab" id="box-preview" aria-controls="box-panel-preview" aria-selected="true" tabindex="0">
Preview
</button>
<button class="tabs-boxed__tab" type="button" role="tab" id="box-code" aria-controls="box-panel-code" aria-selected="false" tabindex="-1">
Code
</button>
<button class="tabs-boxed__tab" type="button" role="tab" id="box-console" aria-controls="box-panel-console" aria-selected="false" tabindex="-1">
Console
</button>
</div>
<div class="tabs-boxed__panel" role="tabpanel" id="box-panel-preview" aria-labelledby="box-preview" tabindex="0">
<p>The rendered result, refreshed on every save.</p>
</div>
<div class="tabs-boxed__panel" role="tabpanel" id="box-panel-code" aria-labelledby="box-code" tabindex="0" hidden>
<p>142 lines of TypeScript across three modules.</p>
</div>
<div class="tabs-boxed__panel" role="tabpanel" id="box-panel-console" aria-labelledby="box-console" tabindex="0" hidden>
<p>No errors. Two warnings about unused imports.</p>
</div>
</div>
<script>
(function () {
document.querySelectorAll('[data-tabs-boxed]').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));
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 seam is the entire trick: the tablist draws a hairline, each tab sits on it with `-mb-px`, and the selected tab paints its own bottom border in the panel's background colour so the two read as one sheet. If a gap or a line reappears, it is because the tab's `border-b` colour and the panel's `bg` have drifted apart - they must match in both light and dark. Suits a code/preview switcher where the panel is a visible surface rather than bare prose.