Back to catalog
Component detail entry
Navigation
@vean/ui
STabs

Tabs

Tabs belongs to the Navigation category. Start with demos and usage, then use the API tables to finish integration and styling decisions.

Use the quick jump panel to reach overview, usage, demos, and API first, then continue through the full page.

Quick jump

Related components

If you are composing interactions in the same family, these components usually pair well together.

Tabs

Overview

A set of layered sections of content—known as tab panels—that are displayed one at a time.

Features

  • WAI-ARIA tabs pattern — the list renders as a tablist, triggers as role="tab", panels as role="tabpanel"; the active tab carries aria-selected, each trigger links its panel via aria-controls/aria-labelledby, and aria-orientation reflects the layout direction.
  • Data-driven compact composition — STabs delegates item iteration, default trigger/content composition, and indicator rendering to the generic Aria TabsCompact<T>, which owns the ARIA wiring for every item.
  • Controlled or uncontrolled state — modelValue supports v-model (controlled); defaultValue provides uncontrolled usage backed by useControllableState.
  • Two activation modes — activationMode: 'automatic' activates a tab on focus (ARIA default); 'manual' activates only on click / Enter / Space.
  • Full keyboard navigation — the useRovingFocusGroup hook provides arrow-key (and Home/End) movement that skips disabled tabs; Enter / Space activates the focused tab.
  • Presence-based content mounting — unmountOnHide: true (default) unmounts inactive panels after the exit animation; false keeps every panel mounted but hidden; forceMount keeps a panel in the DOM unconditionally.
  • Animated indicator — a sliding indicator tracks the active tab with CSS variables (--vean-tabs-indicator-size / --vean-tabs-indicator-position), measured via ResizeObserver and re-positioned on value/direction changes; RTL offsets are mirrored automatically.
  • Horizontal and vertical layouts — orientation: 'vertical' stacks the list and runs the indicator along the block axis.
  • Three customization slots — trigger (scoped { ...item, active }), content (scoped { ...item, active }), and indicator.
  • Six visual variants — size (xs…2xl), orientation (horizontal / vertical), shape (square / rounded), fill (full / auto), and enableIndicator (falls back to a solid active-trigger style) via the tabsVariants scv() recipe.
  • Aria composition — TabsRoot / TabsList / TabsTrigger / TabsContent / TabsIndicator / TabsCompact are exported from @vean/aria/tabs for fully custom styled builds.

Usage

STabs now delegates item iteration, default trigger/content composition, and indicator rendering to Aria TabsCompact. For unstyled, data-driven usage, import TabsCompact from @vean/aria/tabs.

Demos

Basic

The Tab Content: 1

Custom

The Tab Content: 1
The Tab Content: 1

API

Aria Layer

7

Tabs<T>

18 Props1 Emits3 Slots
Props
PropTypeDefaultDescription
class
ClassValue
—
Additional class names applied to the root element.
size
ThemeSize
—
Visual size of the component.
ui
Partial<TabsUi>
—
Styled tabs ui slots, including `indicatorContent` for the compact indicator body.
fill
TabsFill
—
Fill.
shape
TabsShape
—
Shape of the component.
items
T[]
—
Items rendered by the component.
enableIndicator
boolean
—
Whether to enable indicator.
listProps
TabsListProps
—
Properties forwarded to the list element.
triggerProps
TabsTriggerProps
—
Properties forwarded to the trigger element.
contentProps
TabsContentProps
—
Properties forwarded to the content element.
indicatorProps
TabsIndicatorProps
—
Properties forwarded to the indicator element.
modelValue
T['value'] | null
—
The controlled value of the tab to activate. Can be bind as `v-model`.
defaultValue
T['value'] | null
—
The value of the tab that should be active when initially rendered. Use when you do not need to control the state of the tabs
activationMode
TabsActivationMode
automatic
Whether a tab is activated automatically (on focus) or manually (on click).
unmountOnHide
boolean
`true`
When `true`, the element will be unmounted on closed state.
dir
Direction
—
The direction of navigation between items.
loop
boolean
false
Whether keyboard navigation should loop around
orientation
DataOrientation
—
The orientation of the group. Mainly so arrow navigation is done accordingly (left & right vs. up & down)
Emits
Emit NameParametersDescription
update:modelValue
Exclude<T, undefined>
Event handler called when the value changes
Slots
Slot NameParametersDescription
trigger
T & { selected: boolean; }
Custom content for the trigger slot.
content
T & { selected: boolean; }
Custom content for the content slot.
indicator
—
Custom content for the indicator slot.

TabsCompact<T>

13 Props1 Emits3 Slots
Props
PropTypeDefaultDescription
items
T[]
—
Items rendered by the component.
enableIndicator
boolean
—
Whether to enable indicator.
listProps
TabsListProps
—
Properties forwarded to the list element.
triggerProps
TabsTriggerProps
—
Properties forwarded to the trigger element.
contentProps
TabsContentProps
—
Properties forwarded to the content element.
indicatorProps
TabsIndicatorProps
—
Properties forwarded to the indicator element.
modelValue
T['value'] | null
—
The controlled value of the tab to activate. Can be bind as `v-model`.
defaultValue
T['value'] | null
—
The value of the tab that should be active when initially rendered. Use when you do not need to control the state of the tabs
activationMode
TabsActivationMode
automatic
Whether a tab is activated automatically (on focus) or manually (on click).
unmountOnHide
boolean
`true`
When `true`, the element will be unmounted on closed state.
dir
Direction
—
The direction of navigation between items.
loop
boolean
false
Whether keyboard navigation should loop around
orientation
DataOrientation
—
The orientation of the group. Mainly so arrow navigation is done accordingly (left & right vs. up & down)
Emits
Emit NameParametersDescription
update:modelValue
Exclude<T, undefined>
Event handler called when the value changes
Slots
Slot NameParametersDescription
trigger
T & { selected: boolean; }
Custom content for the trigger slot.
content
T & { selected: boolean; }
Custom content for the content slot.
indicator
—
Custom content for the indicator slot.

TabsContent

2 Props
Props
PropTypeDefaultDescription
value
DefinedValue
—
A unique value that associates the content with a trigger.
forceMount
boolean
—
Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

TabsIndicator

2 Props
Props
PropTypeDefaultDescription
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`

TabsList

TabsRoot<T>

7 Props1 Emits
Props
PropTypeDefaultDescription
modelValue
T
—
The controlled value of the tab to activate. Can be bind as `v-model`.
defaultValue
T
—
The value of the tab that should be active when initially rendered. Use when you do not need to control the state of the tabs
activationMode
TabsActivationMode
automatic
Whether a tab is activated automatically (on focus) or manually (on click).
unmountOnHide
boolean
`true`
When `true`, the element will be unmounted on closed state.
dir
Direction
—
The direction of navigation between items.
loop
boolean
false
Whether keyboard navigation should loop around
orientation
DataOrientation
—
The orientation of the group. Mainly so arrow navigation is done accordingly (left & right vs. up & down)
Emits
Emit NameParametersDescription
update:modelValue
Exclude<T, undefined>
Event handler called when the value changes

TabsTrigger

4 Props
Props
PropTypeDefaultDescription
value
DefinedValue
—
A unique value that associates the trigger with a content.
disabled
boolean
—
When `true`, prevents the user from interacting with the tab.
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`

Notes

Architecture and benchmark comparison

ConcernVeanUIshadcn-vue / Radix TabsAnt Design TabsElement Plus Tabs
Aria / styled separation✅ @vean/aria/tabs + scv()✅ Aria primitives❌ single package❌ single package
Data-driven compact API✅ generic TabsCompact<T> + items✅ TabList/Tab/TabPanel parts✅ config-driven (items)✅ config-driven
Controlled / uncontrolled✅ modelValue / defaultValue✅ modelValue / defaultValue✅ activeKey / defaultActiveKey✅ v-model
Activation mode✅ automatic / manual✅❌ (always automatic)❌ (always automatic)
Keyboard navigation✅ roving focus (arrows/Home/End) + Enter/Space✅ roving focus✅ arrows / Home / End✅ arrows
Disabled tab skipping✅ roving focus skips disabled items✅✅✅
Presence / forceMount content✅ unmountOnHide + forceMount + exit animation✅ forceMount❌❌
Animated indicator✅ CSS vars + ResizeObserver + RTL mirroring❌ (no built-in indicator)✅ ink bar (line type)❌
Horizontal + vertical✅ orientation✅✅ tabPosition✅ tab-position
Variants✅ size × orientation × shape × fill❌ (style your own)✅ type / size / tabBarGutter✅ type / size
Custom slots✅ trigger / content / indicator✅ part-level✅ label / children✅ label / icon
ARIA wiring (controls/labelledby)✅ automatic per item✅ automatic✅✅

Runtime considerations

  1. Activation on focus — with activationMode: 'automatic' (default), focusing a tab via keyboard (or ArrowRight/ArrowLeft) activates it; with 'manual' only clicking or pressing Enter/Space activates.
  2. Indicator measurement — the indicator positions itself against the active trigger after the list ref is attached, on value/direction changes, and on resize; it renders only after the first successful measurement.
  3. RTL positioning — in dir: 'rtl' the indicator offset is mirrored (list.clientWidth - offsetLeft - offsetWidth) and the CSS rtl:-translate-x-… flips the translation, so the indicator stays anchored to the active tab.
  4. Content presence — with unmountOnHide: true, inactive panel content is removed after the exit animation; with false it stays mounted but carries the hidden attribute; forceMount keeps the panel in the DOM while still honoring hidden for inactive panels.
  5. Controlled vs. uncontrolled — when modelValue is provided, internal writes only emit update:modelValue; the DOM follows the prop, so external changes re-render automatically.
  6. Disabled tabs — a disabled trigger keeps focusability only for roving focus (data-disabled, not the native disabled attribute), so arrow navigation can skip it; mousedown and key activation are blocked.
  7. ARIA ids — triggers and panels are linked with deterministic ids (soybean-tabs-trigger-{value} / soybean-tabs-content-{value}); aria-controls is only emitted once the matching panel registers itself.

FAQ

How do I switch from automatic to manual activation?

Set activationMode="manual". In manual mode focusing a tab no longer activates it; users activate a tab by clicking or pressing Enter/Space. This follows the WAI-ARIA tabs pattern for panels that are slow to render.

Why doesn’t the indicator appear immediately?

The indicator needs a first measurement of the active trigger (offset + size). It renders right after the list element is attached or the value changes. In a real browser a ResizeObserver notification also triggers a re-measure, keeping it aligned on resize.

What does unmountOnHide control?

With true (default), switching tabs removes inactive panel content from the DOM after the exit animation — best for performance. With false, every panel stays mounted but inactive ones are hidden — best for preserving state (scroll positions, form inputs).

How is keyboard navigation handled?

The list is a roving-focus group: ArrowLeft/ArrowRight (or ArrowUp/ArrowDown in vertical mode) move focus, Home/End jump to the first/last tab, disabled tabs are skipped, and Enter/Space activates the focused tab.

Can I fully customize the tab rendering?

Yes — the trigger slot receives scoped { ...item, active } props, the content slot { ...item, active }, and indicator replaces the indicator body. For a completely custom structure, compose TabsRoot / TabsList / TabsTrigger / TabsContent / TabsIndicator from @vean/aria/tabs and inject styles via provideTabsUi (or STabs’s ui prop).

Do I need to wire aria-controls myself?

No. Each trigger automatically points aria-controls at its panel id (soybean-tabs-content-{value}) once the panel mounts, and each panel sets aria-labelledby back to its trigger — both stay in sync when items are added or removed.

Changelog

Version history is grouped by release so you can quickly see what changed for this component across published versions.

v0.50.0

2026-10-02

Compare release

Style

add size prop and adjust styles for page tabs

Scope: tabsAuthors: soybeanjsView commitc6aee

Optimization

add size prop and adjust styles for page tabs

Scope: tabsAuthors: soybeanjsView commitc6aee

v0.8.0

2026-02-10

Compare release

Feature

add registerContentId and unregisterContentId for dynamic content management

Scope: tabsAuthors: soybeanjsView commitc58fa

v0.5.3

2026-02-05

Compare release

Fix

add nextTick to watchEffect to get dom

Scope: tabsAuthors: soybeanjsView commit00dcd

v0.0.2

2025-11-18

Compare release

Feature

introduce tab components with customizable styles and functionality, including horizontal, vertical, and fill options

Scope: tabsAuthors: soybeanjsView commitc407c

Style

rename theme variable to themeContext for consistency across tab components

Scope: tabsAuthors: soybeanjsView commit084e8

Feature

introduce tab components with customizable styles and functionality, including horizontal, vertical, and fill options

Scope: tabsAuthors: soybeanjsView commitc407c

Style

rename theme variable to themeContext for consistency across tab components

Scope: tabsAuthors: soybeanjsView commit084e8