Tabs
Tabs organize content into multiple sections and allow users to navigate between them.
Usage
<script setup lang="ts">
import { Tabs } from '@hareui/vue'
</script><script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = [ { label: 'Overview', value: 'overview', content: 'View your project overview and recent activity.' }, { label: 'Analytics', value: 'analytics', content: 'Track your metrics and analyze performance data.' }, { label: 'Reports', value: 'reports', content: 'Generate and download detailed reports.' },]</script><template> <Tabs class="w-full max-w-md" :items="items" default-value="overview" :ui="{ tabPanel: 'pt-4' }" /></template>Anatomy
Tabs renders a list container wrapping the tab list, one trigger per items entry (plus an optional separator, and the shared sliding indicator), followed by a panel for the selected item.
<template>
<Tabs :items="items" default-value="…" separator> <!-- separator: optional -->
<template #tab="{ item, index, selected }" /> <!-- replaces item.label -->
<template #content="{ item, index }" /> <!-- fallback panel content -->
<template #[item.slot]="{ item, index }" /> <!-- per-item panel content -->
</Tabs>
</template>The tab list sits in a scroller ([data-slot="scroll-shadow"]). When the tabs overflow it, the edges fade and previous/next chevrons (scrollPrev / scrollNext) appear.
ui key | data-slot | Element |
|---|---|---|
base | tabs | root element |
tabListContainer | tabs-list-container | wrapper around the tab list |
scroller | scroll-shadow | scroll container around the tab list, fades overflowing edges |
scrollPrev | – | chevron that scrolls to previous tabs (shown only when there are any) |
scrollNext | – | chevron that scrolls to next tabs (shown only when there are any) |
tabList | tabs-list | the TabsList |
tab | tabs-tab | per-item TabsTrigger |
tabIndicator | tabs-indicator | sliding selection indicator, positioned via --tabs-indicator-{x,y,width,height} |
separator | tabs-separator | divider inside every tab (iff separator); hidden on the selected tab and the one after it |
tabPanel | tabs-panel | the selected item's TabsContent |
Typing items
items takes the exported TabsItem type. Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.
import type { TabsItem } from '@hareui/vue'
interface MyItem extends TabsItem {
icon: string
}
const items: MyItem[] = [/* ... */]
// <template #tab="{ item }"> — item.icon is a stringExamples
Vertical
Account Settings
Manage your account information and preferences.
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'interface Section extends TabsItem { heading: string}const items: Section[] = [ { label: 'Account', value: 'account', heading: 'Account Settings', content: 'Manage your account information and preferences.' }, { label: 'Security', value: 'security', heading: 'Security Settings', content: 'Configure two-factor authentication and password settings.' }, { label: 'Notifications', value: 'notifications', heading: 'Notification Preferences', content: 'Choose how and when you want to receive notifications.' }, { label: 'Billing', value: 'billing', heading: 'Billing Information', content: 'View and manage your subscription and payment methods.' },]</script><template> <Tabs class="w-full max-w-lg" :items="items" default-value="account" orientation="vertical" :ui="{ tabPanel: 'px-4' }"> <template #content="{ item }"> <h3 class="mb-2 font-semibold"> {{ item.heading }} </h3> <p class="text-sm text-muted"> {{ item.content }} </p> </template> </Tabs></template>Overflow
When the tab list exceeds the available space, the edges fade and scroll chevrons appear so users can navigate the hidden tabs. This works the same way for both horizontal and vertical orientations.
Overview panel content.
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = ['Overview', 'Analytics', 'Reports', 'Performance', 'Engagement', 'Audience', 'Acquisition', 'Retention', 'Settings'] .map(label => ({ label, value: label.toLowerCase(), content: `${label} panel content.` }))</script><template> <div class="w-[400px]"> <Tabs :items="items" :ui="{ tabPanel: 'pt-4' }"> <template #content="{ item }"> <p>{{ item.content }}</p> </template> </Tabs> </div></template>Disabled Tab
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = [ { label: 'Active', value: 'active', content: 'This tab is active and can be selected.' }, { label: 'Disabled', value: 'disabled', disabled: true, content: 'This content cannot be accessed.' }, { label: 'Available', value: 'available', content: 'This tab is also available for selection.' },]</script><template> <Tabs class="w-full max-w-md" :items="items" default-value="active" :ui="{ tabPanel: 'pt-4' }" /></template>With Separator
Set separator to display separator lines between tabs.
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = [ { label: 'Overview', value: 'overview', content: 'View your project overview and recent activity.' }, { label: 'Analytics', value: 'analytics', content: 'Track your metrics and analyze performance data.' }, { label: 'Reports', value: 'reports', content: 'Generate and download detailed reports.' },]</script><template> <Tabs class="w-full max-w-md" :items="items" default-value="overview" separator :ui="{ tabPanel: 'pt-4' }" /></template>Secondary Variant
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = [ { label: 'Overview', value: 'overview', content: 'View your project overview and recent activity.' }, { label: 'Analytics', value: 'analytics', content: 'Track your metrics and analyze performance data.' }, { label: 'Reports', value: 'reports', content: 'Generate and download detailed reports.' },]</script><template> <Tabs class="w-full max-w-md" :items="items" default-value="overview" variant="secondary" :ui="{ tabPanel: 'pt-4' }" /></template>Secondary Variant Vertical
Account Settings
Manage your account information and preferences.
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'interface Section extends TabsItem { heading: string}const items: Section[] = [ { label: 'Account', value: 'account', heading: 'Account Settings', content: 'Manage your account information and preferences.' }, { label: 'Security', value: 'security', heading: 'Security Settings', content: 'Configure two-factor authentication and password settings.' }, { label: 'Notifications', value: 'notifications', heading: 'Notification Preferences', content: 'Choose how and when you want to receive notifications.' }, { label: 'Billing', value: 'billing', heading: 'Billing Information', content: 'View and manage your subscription and payment methods.' },]</script><template> <Tabs class="w-full max-w-lg" :items="items" default-value="account" orientation="vertical" variant="secondary" :ui="{ tabPanel: 'px-4' }"> <template #content="{ item }"> <h3 class="mb-2 font-semibold"> {{ item.heading }} </h3> <p class="text-sm text-muted"> {{ item.content }} </p> </template> </Tabs></template>Alignment
Tab content is centered by default. Use align to align it to the start or end instead, which is especially useful for vertical, sidebar-style navigation. The tabs can still be overridden with :ui="{ tab: '…' }".
General
Manage your account information and preferences.
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'interface Section extends TabsItem { heading: string}const items: Section[] = [ { label: 'General', value: 'general', heading: 'General', content: 'Manage your account information and preferences.' }, { label: 'Subscription & Billing', value: 'billing', heading: 'Subscription & Billing', content: 'View and manage your plan and payment methods.' }, { label: 'Appearance', value: 'appearance', heading: 'Appearance', content: 'Choose a theme and adjust the interface density.' }, { label: 'Notifications', value: 'notifications', heading: 'Notifications', content: 'Choose how and when you want to receive notifications.' }, { label: 'Privacy', value: 'privacy', heading: 'Privacy', content: 'Control what you share and who can see your activity.' },]</script><template> <Tabs class="w-full max-w-lg" :items="items" align="start" default-value="general" orientation="vertical" variant="secondary" :ui="{ tabPanel: 'px-4' }"> <template #content="{ item }"> <h3 class="mb-2 font-semibold"> {{ item.heading }} </h3> <p class="text-sm text-muted"> {{ item.content }} </p> </template> </Tabs></template>Customization
Tailwind CSS
<script setup lang="ts">import type { TabsItem } from '@hareui/vue'import { Tabs } from '@hareui/vue'const items: TabsItem[] = [ { label: 'Monthly', value: 'monthly', content: 'Billed monthly, cancel anytime.' }, { label: 'Yearly', value: 'yearly', content: 'Save 20% with annual billing.' },]// HeroUI's `data-[hovered]` / `data-[pressed]` (React Aria) map to `hover:` / `active:`; the theme's// own hover dimming is a higher-specificity rule, so `opacity-100!` keeps HeroUI's "no dimming" look.const tabClass = [ 'rounded-lg bg-transparent text-muted opacity-100! transition-colors shadow-none', 'hover:not-data-[state=active]:bg-accent-soft', 'hover:not-data-[state=active]:text-accent-soft-foreground', 'active:not-data-[state=active]:bg-accent-soft-hover', 'focus-visible:ring-2 focus-visible:ring-accent/15', 'data-[state=active]:font-medium data-[state=active]:text-accent-foreground',].join(' ')</script><template> <Tabs class="w-full max-w-sm" :items="items" default-value="monthly" :ui="{ tabListContainer: 'rounded-none bg-transparent', tabList: 'rounded-xl border border-accent/10 bg-accent-soft/30 p-1', tab: tabClass, tabIndicator: 'rounded-lg bg-accent shadow-none', tabPanel: 'pt-3 text-sm text-muted', }" /></template>Global Configuration
To customize the Tabs classes for the whole app, extend the tabs theme in createHareUI:
app.use(createHareUI({
ui: {
tabs: {
slots: {
tabList: 'rounded-xl border border-border bg-surface p-1',
tab: 'rounded-lg font-medium',
tabIndicator: 'rounded-lg bg-foreground shadow-sm',
},
variants: {
variant: {
secondary: {
tabList: 'border-b border-border bg-transparent p-0',
tabIndicator: 'rounded-none bg-accent',
},
},
},
},
},
}))Styling Reference
Slots
base→[data-slot="tabs"]– root container (flex, column in horizontal orientation)tabListContainer→[data-slot="tabs-list-container"]– rounded background behind the listscroller→[data-slot="scroll-shadow"]– scroll area around the list (scrollbar hidden); fades overflowing edges and setsdata-left-scroll/data-right-scroll(data-top-scroll/data-bottom-scrollwhen vertical,data-left-right-scroll/data-top-bottom-scrollwhen both)scrollPrev/scrollNext– chevron buttons, shown only while there's hidden content on that sidetabList→[data-slot="tabs-list"]– the row/column of triggerstab→[data-slot="tabs-tab"]– one trigger per itemseparator→[data-slot="tabs-separator"]– divider rendered in every tab whenseparatoris set; hidden (opacity-0) on the selected tab and the one following ittabPanel→[data-slot="tabs-panel"]– only the selected item's panel is mountedtabIndicator→[data-slot="tabs-indicator"]– sliding selection indicator; its box comes from the selected tab'sgetBoundingClientRect()via--tabs-indicator-x/-y/-width/-height
Orientation Attributes
[data-orientation="horizontal"]– horizontal tab layout (default)[data-orientation="vertical"]– vertical tab layout
Variants
variant="secondary"– underline indicator instead of the filled pill
Alignment
align="start"– aligns tab content to the startalign="end"– aligns tab content to the end
Interactive States
Tabs sets these data attributes (from Reka UI):
- Selected:
[data-state="active"]on[data-slot="tabs-tab"] - Hover:
:hover - Focus:
:focus-visible - Disabled:
[data-disabled]on[data-slot="tabs-tab"](fromitem.disabled)
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | TabsItem[] | - | The tabs to render. |
modelValue | string | number | - | The selected tab value. Use v-model to control it. |
defaultValue | string | number | items[0]?.value ?? '0' | The value of the tab selected by default (uncontrolled). |
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction of the tab list. |
variant | "primary" | "secondary" | 'primary' | Visual style of the tabs. |
align | "center" | "start" | "end" | 'center' | Alignment of the tab labels. |
content | boolean | true | Whether to render a panel for every item. |
separator | boolean | false | Whether to render a separator between tabs. HeroUI separators are opt-in
via <Tabs.Separator/>. |
ui | ComponentSlots<{ slots: { base: string; tabListContainer: string; scroller: string; scrollPrev: string[]; scrollNext: string[]; tabList: string; tab: string[]; tabIndicator: string[]; tabPanel: string; separator: string[]; }; variants: { align: { center: {}; start: { tab: string; tabIndicator: string; }; end: { tab: string; tabIndicator: string; }; }; variant: { primary: {}; secondary: { tabListContainer: string; tabList: string; tab: string; tabIndicator: string; separator: string; }; }; orientation: { horizontal: { scrollPrev: string; scrollNext: string; }; vertical: { scroller: string; scrollPrev: string; scrollNext: string; }; }; }; compoundVariants: { variant: string; orientation: string; class: { tabListContainer: string; tabIndicator: string; }; }[]; defaultVariants: { align: string; variant: string; orientation: string; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
tab | { item: TabsItem; index: number; selected: boolean; } | Replaces a tab's label. |
content | { item: TabsItem; index: number; } | Fallback panel content for every item, overridden by [item.slot]. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: string | number | undefined] | - |