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>
View your project overview and recent activity.
<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 keydata-slotElement
basetabsroot element
tabListContainertabs-list-containerwrapper around the tab list
scrollerscroll-shadowscroll 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)
tabListtabs-listthe TabsList
tabtabs-tabper-item TabsTrigger
tabIndicatortabs-indicatorsliding selection indicator, positioned via --tabs-indicator-{x,y,width,height}
separatortabs-separatordivider inside every tab (iff separator); hidden on the selected tab and the one after it
tabPaneltabs-panelthe 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 string

Examples

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

This tab is active and can be selected.
<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.

View your project overview and recent activity.
<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

View your project overview and recent activity.
<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

Billed monthly, cancel anytime.
<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 list
  • scroller → [data-slot="scroll-shadow"] – scroll area around the list (scrollbar hidden); fades overflowing edges and sets data-left-scroll / data-right-scroll (data-top-scroll / data-bottom-scroll when vertical, data-left-right-scroll / data-top-bottom-scroll when both)
  • scrollPrev / scrollNext – chevron buttons, shown only while there's hidden content on that side
  • tabList → [data-slot="tabs-list"] – the row/column of triggers
  • tab → [data-slot="tabs-tab"] – one trigger per item
  • separator → [data-slot="tabs-separator"] – divider rendered in every tab when separator is set; hidden (opacity-0) on the selected tab and the one following it
  • tabPanel → [data-slot="tabs-panel"] – only the selected item's panel is mounted
  • tabIndicator → [data-slot="tabs-indicator"] – sliding selection indicator; its box comes from the selected tab's getBoundingClientRect() 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 start
  • align="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"] (from item.disabled)

API Reference

Props

PropTypeDefaultDescription
itemsTabsItem[]-The tabs to render.
modelValuestring | number-The selected tab value. Use v-model to control it.
defaultValuestring | numberitems[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.
contentbooleantrueWhether to render a panel for every item.
separatorbooleanfalseWhether to render a separator between tabs. HeroUI separators are opt-in via <Tabs.Separator/>.
uiComponentSlots<{ 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

SlotPropsDescription
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

EventPayloadDescription
update:modelValue[value: string | number | undefined]-

On this page

No Headings