PageTabs
Overview
A tabbed interface designed for navigating between different pages or views. It supports features like closable tabs, context menus, and customizable styling.
Features
- Data-driven compact composition —
SPageTabsdelegates the whole structure to the Aria genericPageTabsCompact<T>, which owns tab iteration, pin sorting, close semantics, context-menu wiring, and the default item body (icon + label + pin + close + indicator). - Controlled or uncontrolled state —
modelValue/itemssupportv-model/v-model:items(controlled); omitting them falls back to internal state viauseControllableState. - Closable tabs with async guard — every non-pinned tab renders a close button;
beforeClosemay returnfalseor a promise resolving tofalseto block closing. Closing the active tab activates the next (or previous) sibling; closing via keyboardBackspaceand middle-click (middleClickClose) work out of the box. - Pin / unpin with auto-sorting — pinned tabs sort to the front on any change (pinned group first, then normal tabs;
hidePinnedIconis display-only and never affects ordering); the inline pin button toggles pinned state and the context menu providesPin/Unpin. - Drag-to-reorder — with
draggableenabled, tabs can be dragged horizontally to change their position. Reordering is zone-restricted, matching browser tab bars (Chrome / VS Code): tabs only reorder within their own zone — the pinned group first, then unpinned — so an unpinned tab halts at the pinned boundary in real time and never lands before pinned tabs. A tab item with an explicitdraggable: falseis locked in place: it cannot be dragged and it acts as a barrier, so no other tab can be inserted at its position (e.g. a home tab pinned to the very first slot). Dragging is driven by the AriauseSortableListhook: the dragged tab keeps its own slot as the placeholder and follows the cursor withtransformonly, while the sibling tabs slide out of the way with FLIP; on drop the new order is applied (emittingupdate:items) and the dragged tab slides into its final slot. Keyboard sorting works too — focus a tab, pressSpaceto pick it up, move it with the arrow keys, drop withSpace/Enter, abort withEscape.tabDragStart/tabDragMove/tabDragEndemit the live drag state{ item, index }. - Context-menu factory —
menuFactory(tab, state)receives the hovered tab plus aPageTabsState(close, closeLeft, closeRight, closeOther, closeAll, pin, unpin and their per-action*Closablebooleans) to build custom menus;selectContextMenuemits the chosen action and tab. - Full keyboard support — the
useRovingFocusGrouphook provides arrow-key movement;Enteractivates a tab,Backspacecloses it, andSpacepicks a tab up for keyboard reordering. - Auto-scroll active tab —
usePageTabsScrollkeeps the active tab horizontally centered (scrollTosmooth) and converts the vertical wheel into horizontal scrolling. - Three visual variants —
variant(chrome/card/slider) with per-variant indicators (chrome corner SVGs / slider underline) andsize(xs…2xl) via thepageTabsVariantsscv()recipe. - Six customization slots —
item(scoped{ item, index, active, closable }),icon,label,indicator,pin-icon,close-icon. - Localized accessibility text — the close / pin buttons fall back to localized
aria-labels (closeTab/pinTab/unpinTab) across 13 built-in languages, overridable per button viaaria-label. - Aria composition —
PageTabsRoot/PageTabsItem/PageTabsClose/PageTabsPin/PageTabsCompactare exported from@vean/aria/page-tabsfor fully custom styled builds.
Usage
SPageTabsdelegates its tab management to AriaPageTabsCompact. For unstyled, data-driven composition, importPageTabsCompactfrom@vean/aria/page-tabs.
Demos
Basic
Draggable
Drag tabs to reorder them within their own zone: pinned tabs reorder among themselves at the front, and unpinned tabs cannot be dragged past the pinned group — they halt at the boundary. The first tab sets draggable: false , so it is locked in place and no other tab can be dropped before it.
API
Aria Layer
7PageTabs<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | root element class |
size | ThemeSize | — | Visual size of the component. |
variant | PageTabsVariant | — | Visual variant of the component. |
ui | Partial<PageTabsUi> | — | Per-slot class overrides for the component. |
items | T[] | — | Items rendered by the component. |
menuFactory | ((tab: T, state: PageTabsState) => PageTabsContextMenuOptionData[]) | — | A factory function to generate context menu options for each tab. |
beforeClose | ((value: string) => MaybePromise<boolean | void>) | — | Callback invoked before closing the tab. Return `false` or a promise that resolves to `false` to prevent closing. |
itemProps | BaseProps | — | Per-slot class overrides for the component. |
pinProps | BaseProps | — | Per-slot class overrides for the component. |
closeProps | BaseProps | — | Per-slot class overrides for the component. |
contextMenuProps | Omit<ContextMenuCompactProps<import("@/index").DefinedValue>, 'options'> | — | Additional props for the context menu component. |
modelValue | string | — | The active tab value. |
middleClickClose | boolean | — | Whether middle-clicking a tab closes it. |
draggable | boolean | — | Whether tabs can be reordered by dragging. Tabs only reorder within
their own zone — the pinned group always stays in front. A tab item
with an explicit `draggable: false` is locked in place. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | The direction of navigation between items. |
loop | boolean | false | Whether keyboard navigation should loop around |
currentTabStopId | string | null | — | The controlled value of the current stop item. Can be bound as `v-model`. |
defaultCurrentTabStopId | string | — | The value of the current stop item.
Use when you do not need to control the state of the stop item. |
preventScrollOnEntryFocus | boolean | — | When `true`, will prevent scrolling to the focus item when focused. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | PageTabsCompactItemSlotProps<T> | Custom content for the default tab item body. |
icon | PageTabsCompactItemSlotProps<T> | Custom content for the tab icon. |
label | PageTabsCompactItemSlotProps<T> | Custom content for the tab label. |
indicator | PageTabsCompactItemSlotProps<T> | Custom content for the tab indicator. |
pin-icon | (() => void) | Custom content for the pin icon. |
close-icon | (() => void) | Custom content for the close icon. |
PageTabsClose
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
PageTabsCompact<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Items rendered by the component. |
menuFactory | ((tab: T, state: PageTabsState) => PageTabsContextMenuOptionData[]) | — | A factory function to generate context menu options for each tab. |
beforeClose | ((value: string) => MaybePromise<boolean | void>) | — | Callback invoked before closing the tab. Return `false` or a promise that resolves to `false` to prevent closing. |
itemProps | BaseProps | — | Per-slot class overrides for the component. |
pinProps | BaseProps | — | Per-slot class overrides for the component. |
closeProps | BaseProps | — | Per-slot class overrides for the component. |
contextMenuProps | Omit<ContextMenuCompactProps<import("@/index").DefinedValue>, 'options'> | — | Additional props for the context menu component. |
modelValue | string | — | The active tab value. |
middleClickClose | boolean | — | Whether middle-clicking a tab closes it. |
draggable | boolean | — | Whether tabs can be reordered by dragging. Tabs only reorder within
their own zone — the pinned group always stays in front. A tab item
with an explicit `draggable: false` is locked in place. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | The direction of navigation between items. |
loop | boolean | false | Whether keyboard navigation should loop around |
currentTabStopId | string | null | — | The controlled value of the current stop item. Can be bound as `v-model`. |
defaultCurrentTabStopId | string | — | The value of the current stop item.
Use when you do not need to control the state of the stop item. |
preventScrollOnEntryFocus | boolean | — | When `true`, will prevent scrolling to the focus item when focused. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | PageTabsCompactItemSlotProps<T> | Custom content for the default tab item body. |
icon | PageTabsCompactItemSlotProps<T> | Custom content for the tab icon. |
label | PageTabsCompactItemSlotProps<T> | Custom content for the tab label. |
indicator | PageTabsCompactItemSlotProps<T> | Custom content for the tab indicator. |
pin-icon | (() => void) | Custom content for the pin icon. |
close-icon | (() => void) | Custom content for the close icon. |
PageTabsCompactItem
PageTabsItem
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The unique value of the tab. |
pinned | boolean | — | Whether the tab is pinned. |
draggable | boolean | — | Whether drag reordering is enabled for this tab. A non-draggable tab is
locked in place: it cannot be dragged and other tabs cannot be dropped
onto it. Pinned tabs reorder within the pinned zone only. |
index | number | — | Index of the tab within `items`, used to build the drag payload. |
PageTabsPin
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
PageTabsRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | The active tab value. |
middleClickClose | boolean | — | Whether middle-clicking a tab closes it. |
draggable | boolean | — | Whether tabs can be reordered by dragging. Tabs only reorder within
their own zone — the pinned group always stays in front. A tab item
with an explicit `draggable: false` is locked in place. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | The direction of navigation between items. |
loop | boolean | false | Whether keyboard navigation should loop around |
currentTabStopId | string | null | — | The controlled value of the current stop item. Can be bound as `v-model`. |
defaultCurrentTabStopId | string | — | The value of the current stop item.
Use when you do not need to control the state of the stop item. |
preventScrollOnEntryFocus | boolean | — | When `true`, will prevent scrolling to the focus item when focused. |
Notes
Architecture and benchmark comparison
| Concern | VeanUI | Ant Design Tabs | Element Plus Tabs | tags-view (vue-element-admin style) |
|---|---|---|---|---|
| Aria / styled separation | ✅ @vean/aria/page-tabs + scv() | ❌ single package | ❌ single package | ❌ custom per app |
| Data-driven compact API | ✅ generic PageTabsCompact<T> + items | ✅ config-driven (items) | ✅ config-driven | ✅ per app |
| Controlled / uncontrolled | ✅ modelValue/items + useControllableState | ✅ activeKey / defaultActiveKey | ✅ v-model | — |
| Closable + async guard | ✅ beforeClose (false / Promise<false>) | ✅ onEdit + beforeChange | ✅ closable / before-leave | ✅ before-close |
| Pin / unpin + auto-sort | ✅ pinned tabs sort to front | ❌ (custom tabLabel) | ❌ | ✅ (per app, e.g. affix-tab) |
| Drag-to-reorder | ✅ draggable + tabDragStart/Move/End | ❌ | ❌ | ✅ (per app) |
| Context menu (left/right/other/all close) | ✅ menuFactory + PageTabsState | ❌ (dropdownMenu on more only) | ❌ | ✅ (per app) |
| Middle-click close | ✅ middleClickClose | ❌ | ❌ | ✅ (per app) |
| Keyboard (roving focus + Enter/Backspace) | ✅ useRovingFocusGroup | ✅ arrows / Home / End | ✅ arrows | ❌ |
| Auto-scroll active tab | ✅ centered scrollTo + wheel-to-horizontal | ✅ auto on tabBar | ❌ | ✅ (per app) |
| Variants | ✅ chrome/card/slider × xs…2xl | ✅ line/card/editable-card | ✅ card/border-card | — |
Localized aria-label | ✅ locale registry (13 langs) | partial | — | — |
| Slots | ✅ 6 typed slots | ✅ label/closeIcon/… | ✅ label/icon/… | — |
Runtime considerations
- Pinned tabs are not closable —
closablederives from!pinned; pinned tabs render no close button, and middle-click /Backspace/ context-menu close actions are disabled for them. - Active-tab close fallback — closing the active tab activates the next sibling, falling back to the previous one; closing the last tab clears
modelValueto''(no tab active). - Async
beforeClose— it may be synchronous or return a promise; only a resolvedfalseblocks the close. Thecloseevent fires after the guard passes and the tab is removed. - Pin sorting —
sortTabsreorders items as pinned → normal (stable, keeping the relative order within each group) and emitsupdate:itemswhen the order changes; in controlled mode the parent must accept the new order to see the reorder. - Controlled vs. uncontrolled — when
modelValue/itemsprops are provided, internal writes only emitupdate:modelValue/update:items; the DOM follows the props, so external changes re-render automatically. - Context menu target — hovering a tab (pointerenter) sets it as the context target and emits
contextmenu; the menu itself renders frommenuFactoryonly when it returns non-empty options. - Drag-to-reorder scope — drag reordering is a
PageTabsCompact-level feature (draggable). Sorting is zone-restricted: tabs reorder within their own zone only (pinned → unpinned; all pinned tabs — includinghidePinnedIconones — share one zone), so unpinned tabs can never be dragged in front of pinned ones — the real-time reorder is blocked at the boundary andsortTabskeeps the zone invariant. A tab item with an explicitdraggable: falseis fully locked: it cannot be dragged and is excluded from collision candidates, so no other tab can be dropped onto its position. In controlled mode (v-model:items) the parent must accept the reorderedupdate:itemsto see the new order. StandalonePageTabsRoot/PageTabsItemcomposition has no drag support. - Locale fallback — close/pin
aria-labels come fromuseLocaleMessages; missing keys fall back to the default English bundle; an explicitaria-labelon the close/pin button wins.
FAQ
How do I prevent closing a tab?
Return false (or a promise that resolves to false) from beforeClose. The guard applies to the close button, middle-click, Backspace, and the context-menu close actions. For a pinned tab, just mark pinned: true — pinned tabs are never closable.
Why are pinned tabs shown first?
Pinning is meant to keep important pages accessible; sortTabs moves pinned tabs (as one group, hidePinnedIcon included) in front of normal ones while keeping the relative order inside each group. If you pass pinned items in arbitrary order, the component reorders them on mount and emits update:items with the sorted array.
Can I customize the context menu?
Yes — implement menuFactory(tab, state). state exposes close, closeLeft, closeRight, closeOther, closeAll, pin, unpin, and the matching *Closable flags so you can disable irrelevant actions (e.g. “Close Left” on the first tab). Selecting an item runs its action and emits selectContextMenu(menu, tab).
How do I close the active tab gracefully?
Closing the active tab automatically activates the next sibling (or the previous one when the closed tab was last). If the last remaining tab is closed, modelValue becomes '' and no tab is active.
What keyboard shortcuts are supported?
Arrow keys move focus through tabs (roving focus), Enter activates the focused tab, and Backspace closes it (unless pinned or blocked by beforeClose). Middle-click also closes when middleClickClose is enabled.
Can I build a fully custom page tabs?
Yes — compose PageTabsRoot / PageTabsItem / PageTabsClose / PageTabsPin / PageTabsCompact from @vean/aria/page-tabs and inject styles via providePageTabsUi (or SPageTabs’s ui prop). The item slot receives { item, index, active, closable } scoped props for per-tab rendering.
How do I enable drag-to-reorder?
Set draggable on SPageTabs (or PageTabsCompact). Tabs become horizontally draggable with a floating preview and smooth slide animation; the new order is written back through v-model:items. Like browser tab bars, reordering is zone-restricted: pinned tabs (including hidePinnedIcon ones) reorder among themselves at the front, and unpinned tabs cannot be dragged past the pinned group — they halt at the boundary in real time. To lock an individual tab (e.g. a home tab kept at the very first slot), set draggable: false on that item: it cannot be dragged and no other tab can be dropped onto its position.