Imported from falentio/cimi (
apps/web/app/components/ui/tabs/AGENTS.md). Install upstream withnpx skills add falentio/cimi --skill tabs. Copyright stays with the author.
Tabs
An accessible tabs primitive that shows one content panel at a time and supports horizontal or vertical navigation.
Conclusion
Use UITabs as the root, place UITabsList inside it, and put every UITabsTrigger directly inside the list. Give each trigger a unique value and pair it with a UITabsContent that has the same value. Use default-value for initial uncontrolled state and v-model when the parent owns the active tab.
Usage
shadcn-nuxt scans @/components/ui and prefixes component exports with UI, as configured in apps/web/nuxt.config.ts. Use the exact tags below without importing the components.
The smallest valid composition uses one root, one list, one or more triggers, and matching content panels.
<template>
<UITabs default-value="account">
<UITabsList aria-label="Account settings">
<UITabsTrigger value="account">Account</UITabsTrigger>
<UITabsTrigger value="password">Password</UITabsTrigger>
</UITabsList>
<UITabsContent value="account">Account settings.</UITabsContent>
<UITabsContent value="password">Password settings.</UITabsContent>
</UITabs>
</template>
Meaningful Information
Exports and imports
index.tsexportsTabs,TabsContent,TabsList, andTabsTriggerfrom the four local Vue files.- The Nuxt auto-import names are
UITabs,UITabsContent,UITabsList, andUITabsTrigger. Use these tags in templates without component imports. index.tsalso exports the lowercasetabsListVariantshelper. Import it explicitly from@/components/ui/tabswhen JavaScript or TypeScript needs the generated list classes.index.tsexports the type-onlyTabsListVariantstype. Import it explicitly withimport typefrom@/components/ui/tabs.- The local directory does not export
TabsIndicator. Do not useUITabsIndicatorwith this component family.
import { tabsListVariants } from '@/components/ui/tabs'
import type { TabsListVariants } from '@/components/ui/tabs'
Composition and values
- Put
UITabsListinsideUITabs. - Put every
UITabsTriggerdirectly insideUITabsList. Do not render a trigger beside the list. - Put
UITabsContentinsideUITabsalongside the list. - Give every trigger a unique
value. The value accepts a string or number. - Give every content panel the exact value of its trigger. Matching values connect the trigger to its panel.
- Keep trigger labels short and meaningful. Use
aria-labeloraria-labelledbyonUITabsListwhen the visible list has no label.
Root props and state
UITabswraps Reka UITabsRootand forwards its root props, emits, and slot.default-valuemaps todefaultValueand sets the initially active tab for uncontrolled usage. It is read on initial render and does not control later changes.v-modelbindsmodelValueand controls the active tab. The bound value must match one of the trigger and content values.orientationaccepts"horizontal"or"vertical"and defaults to"horizontal". It also determines arrow-key navigation.activation-modemaps toactivationMode, accepts"automatic"or"manual", and defaults to"automatic".diraccepts"ltr"or"rtl". When omitted, the root inherits the configured direction or uses left-to-right direction.unmount-on-hidemaps tounmountOnHideand defaults totrue. Set:unmount-on-hide="false"when inactive content must stay mounted.aschanges the root element.as-childreplaces the root element with one child while preserving tabs behavior.classis merged with the local root classes throughcn.
List props and variants
UITabsListwraps Reka UITabsListand forwards its props.variantis local to this wrapper. It accepts"default"or"line"and defaults to"default".- The
defaultvariant uses a muted list background and a filled active trigger. - The
linevariant removes the list background and marks the active trigger with an underline. loopdefaults totrue. Set:loop="false"to stop arrow navigation at the first or last trigger.as,as-child, andclassare also forwarded or merged by the wrapper.tabsListVariantsis the class-variance-authority helper that defines the localvariantvalues. Use it instead of duplicating the variant class strings.
Trigger props and disabled tabs
UITabsTriggerwraps Reka UITabsTriggerand renders a button by default.valueis required and links the trigger to oneUITabsContentpanel.disableddefaults tofalse. A disabled trigger cannot be clicked or activated from the keyboard, and the tabs roving focus behavior skips it.aschanges the rendered element.as-childmerges tabs behavior into one child element. Keep that child a single accessible interactive control.classis merged with the local trigger classes. The local styles readdata-state,data-disabled,data-orientation, and the parent list variant.
Content props
UITabsContentwraps Reka UITabsContentand renders adivby default.valueis required and must exactly match one trigger value.force-mountmaps toforceMountand keeps a panel mounted when more control is needed for animations. Rootunmount-on-hidecontrols the default inactive-panel behavior.as,as-child, andclassare forwarded or merged by the wrapper.
Events and slots
UITabsemitsupdate:modelValuewith the new active value. Vue maps this event tov-model. Listen with@update:model-valuewhen an explicit handler is needed.UITabsList,UITabsTrigger, andUITabsContentadd no local emits. Their Reka UI props and behavior pass through the wrappers.- The root slot exposes the current
modelValuethrough the Reka UI slot contract.
Keyboard behavior
Tabmoves focus into the active trigger when focus enters the tab list. It moves focus out of the list according to the page tab order.- In a horizontal list,
ArrowLeftandArrowRightmove between triggers. Direction followsdir. - In a vertical list,
ArrowUpandArrowDownmove between triggers. Homemoves focus to the first enabled trigger.Endmoves focus to the last enabled trigger.- With the default
activation-mode="automatic", arrow,Home, andEndnavigation activates the focused trigger. - With
activation-mode="manual", navigation moves focus without changing the active panel. PressEnterorSpaceto activate the focused trigger. looponUITabsListcontrols whether navigation wraps from the last trigger to the first trigger.
Accessibility
- Reka UI supplies the tab list, tab, and tab panel roles and connects each trigger with its matching content panel.
- The active trigger exposes the selected state, and inactive panels are hidden or unmounted according to the root and content mounting props.
- Give the list an accessible name when its visible context does not already label it. Use
aria-labelfor a short label oraria-labelledbyfor a visible heading. - Keep the default trigger button or make an
as-childreplacement keyboard accessible. Do not place a second interactive element inside a trigger. - Automatic activation works best when switching panels does not have noticeable latency. Use manual activation when loading a panel would make focus movement slow.
Gotchas
- Use
default-value,activation-mode,unmount-on-hide, andforce-mountin kebab case in Vue templates. - Changing
default-valueafter mount does not change the active tab. Usev-modelfor reactive control. - A trigger without a matching content value does not display a panel. A content panel without a matching trigger is not reachable through the tab list.
orientation="vertical"changes the Reka UI orientation data and keyboard direction. The local list supplies the vertical column layout, while the root keeps its flex layout.- The
linevariant is aUITabsListprop, not a root prop. Write<UITabsList variant="line">. - The local component family has no item wrapper. Do not add a made-up
UITabsItemaround each trigger.
Examples
Default tabs
Use the default variant when the list should have the muted background treatment.
<template>
<UITabs default-value="account" class="w-full max-w-md">
<UITabsList aria-label="Settings sections">
<UITabsTrigger value="account">Account</UITabsTrigger>
<UITabsTrigger value="password">Password</UITabsTrigger>
</UITabsList>
<UITabsContent value="account">Update your account details.</UITabsContent>
<UITabsContent value="password">Change your password.</UITabsContent>
</UITabs>
</template>
Line variant
Pass variant="line" to the list for the underline treatment.
<template>
<UITabs default-value="overview">
<UITabsList variant="line" aria-label="Project sections">
<UITabsTrigger value="overview">Overview</UITabsTrigger>
<UITabsTrigger value="activity">Activity</UITabsTrigger>
</UITabsList>
<UITabsContent value="overview">Project overview.</UITabsContent>
<UITabsContent value="activity">Recent project activity.</UITabsContent>
</UITabs>
</template>
Vertical tabs
Set orientation="vertical" to place the list beside the content and use up and down arrow navigation.
<template>
<UITabs default-value="profile" orientation="vertical" class="flex-row">
<UITabsList aria-label="Profile sections">
<UITabsTrigger value="profile">Profile</UITabsTrigger>
<UITabsTrigger value="notifications">Notifications</UITabsTrigger>
<UITabsTrigger value="security">Security</UITabsTrigger>
</UITabsList>
<UITabsContent value="profile">Profile settings.</UITabsContent>
<UITabsContent value="notifications">Notification settings.</UITabsContent>
<UITabsContent value="security">Security settings.</UITabsContent>
</UITabs>
</template>
Controlled tabs
Bind v-model when another part of the page needs to read or change the active tab.
<script setup lang="ts">
import { ref } from 'vue'
const activeTab = ref('account')
</script>
<template>
<UITabs v-model="activeTab">
<UITabsList aria-label="Account settings">
<UITabsTrigger value="account">Account</UITabsTrigger>
<UITabsTrigger value="password">Password</UITabsTrigger>
</UITabsList>
<UITabsContent value="account">Account settings.</UITabsContent>
<UITabsContent value="password">Password settings.</UITabsContent>
</UITabs>
</template>