Skip to content

@carrot/design-vue-tabs

Vue 3 tab components for the Carrot Design System. Compound component architecture with configurable panel mount strategies, keyboard navigation, and full ARIA tab pattern.

Installation

ts
import { CarrotTabs, CarrotTabList, CarrotTab, CarrotTabPanels, CarrotTabPanel } from "@carrot/design-vue-tabs";

Requires the CSS layer from @carrot/design-components-tabs.


Components

CarrotTabs

Root container that provides shared context to all tab children via inject.

Usage

vue
<!-- Basic (uncontrolled) -->
<CarrotTabs default-value="overview">
  <CarrotTabList>
    <CarrotTab name="overview">Overview</CarrotTab>
    <CarrotTab name="specs">Specifications</CarrotTab>
    <CarrotTab name="reviews">Reviews</CarrotTab>
  </CarrotTabList>
  <CarrotTabPanels>
    <CarrotTabPanel name="overview">Overview content</CarrotTabPanel>
    <CarrotTabPanel name="specs">Specs content</CarrotTabPanel>
    <CarrotTabPanel name="reviews">Reviews content</CarrotTabPanel>
  </CarrotTabPanels>
</CarrotTabs>

<!-- Controlled -->
<CarrotTabs v-model="activeTab">
  <CarrotTabList>
    <CarrotTab name="one">Tab 1</CarrotTab>
    <CarrotTab name="two">Tab 2</CarrotTab>
  </CarrotTabList>
  <CarrotTabPanels>
    <CarrotTabPanel name="one">Content 1</CarrotTabPanel>
    <CarrotTabPanel name="two">Content 2</CarrotTabPanel>
  </CarrotTabPanels>
</CarrotTabs>

<!-- Mount strategies -->
<CarrotTabs mount-strategy="lazy">...</CarrotTabs>
<!-- Mount on first activation, keep (default) -->
<CarrotTabs mount-strategy="eager">...</CarrotTabs>
<!-- Mount all immediately -->
<CarrotTabs mount-strategy="unmount">...</CarrotTabs>
<!-- Unmount when deactivated -->

Props

PropTypeDefaultDescription
modelValuestringActive tab name (v-model)
defaultValuestringfirst tabDefault tab when uncontrolled
mountStrategyTabMountStrategy"lazy"Panel mount/unmount behaviour

Events

EventPayloadDescription
update:modelValuestringActive tab changed
changestringActive tab changed

CarrotTabList

Horizontal tab bar with keyboard navigation and variant styling.

Usage

vue
<!-- Underline (default) -->
<CarrotTabList>
  <CarrotTab name="a">Tab A</CarrotTab>
  <CarrotTab name="b">Tab B</CarrotTab>
</CarrotTabList>

<!-- Variants -->
<CarrotTabList variant="pills">...</CarrotTabList>
<CarrotTabList variant="underline">...</CarrotTabList>

<!-- Scrollable (overflow) -->
<CarrotTabList scrollable>...</CarrotTabList>

<!-- Full width (stretch to fill) -->
<CarrotTabList full>...</CarrotTabList>

Props

PropTypeDefaultDescription
variantTabsVariantVisual style
scrollablebooleanfalseHorizontal scroll on overflow
fullbooleanfalseStretch tabs to fill width
ariaLabelstringAccessible label for the tablist

CarrotTab

Individual tab button. Auto-registers with the parent CarrotTabs on mount.

Usage

vue
<!-- Basic -->
<CarrotTab name="settings">Settings</CarrotTab>

<!-- With icon -->
<CarrotTab name="settings">
  <template #icon><SettingsIcon /></template>
  Settings
</CarrotTab>

<!-- With badge -->
<CarrotTab name="notifications" badge="3">Notifications</CarrotTab>

<!-- Badge slot -->
<CarrotTab name="notifications">
  Notifications
  <template #badge><CarrotBadgeCount :count="unread" /></template>
</CarrotTab>

<!-- Disabled -->
<CarrotTab name="admin" disabled>Admin</CarrotTab>

Props

PropTypeDefaultDescription
namestringrequiredUnique identifier (must match a panel)
disabledbooleanfalseDisabled state
badgestringBadge text

Slots

SlotDescription
defaultTab label
iconLeading icon
badgeTrailing badge content

CarrotTabPanels

Wrapper for tab panels. Thin container — no logic, just layout.

vue
<CarrotTabPanels>
  <CarrotTabPanel name="one">...</CarrotTabPanel>
  <CarrotTabPanel name="two">...</CarrotTabPanel>
</CarrotTabPanels>

CarrotTabPanel

Content panel that mounts/shows based on the active tab and mount strategy.

vue
<CarrotTabPanel name="settings">
  <h2>Settings</h2>
  <p>Panel content here.</p>
</CarrotTabPanel>

Props

PropTypeDefaultDescription
namestringrequiredMust match a CarrotTab's name

Mount Strategies

StrategyBehaviourUse When
lazy (default)Mount on first activation, stay mountedMost cases — preserves state, avoids upfront cost
eagerAll panels mounted immediately, toggled with v-showSEO, or panels need to initialise in background
unmountMount on activation, unmount on deactivationFresh state needed each time (e.g. forms that should reset)

Keyboard Navigation

KeyAction
ArrowRight / ArrowDownNext tab (skips disabled, wraps)
ArrowLeft / ArrowUpPrevious tab (skips disabled, wraps)
HomeFirst enabled tab
EndLast enabled tab

Focus follows selection — selecting a tab via keyboard also moves focus to it.


ARIA

The implementation follows the WAI-ARIA Tabs Pattern:

  • CarrotTabListrole="tablist"
  • CarrotTabrole="tab", aria-selected, aria-controls, roving tabindex
  • CarrotTabPanelrole="tabpanel", aria-labelledby
  • IDs auto-generated with unique prefix per CarrotTabs instance

Dependencies

PackagePurpose
@carrot/design-components-tabsCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.


Usage Guide

Setup

ts
import { CarrotTabs, CarrotTabList, CarrotTab, CarrotTabPanels, CarrotTabPanel } from "@carrot/design-vue-tabs";
import "@carrot/design-components-tabs";

CarrotTabs — Practical Examples

Product Detail Tabs (Uncontrolled)

vue
<script setup lang="ts">
import {
  CarrotTabs, CarrotTabList, CarrotTab,
  CarrotTabPanels, CarrotTabPanel,
} from "@carrot/design-vue-tabs";
</script>

<template>
  <CarrotTabs default-value="overview">
    <CarrotTabList aria-label="Product information">
      <CarrotTab name="overview">Overview</CarrotTab>
      <CarrotTab name="specs">Specifications</CarrotTab>
      <CarrotTab name="reviews">Reviews (24)</CarrotTab>
      <CarrotTab name="shipping">Shipping</CarrotTab>
    </CarrotTabList>

    <CarrotTabPanels>
      <CarrotTabPanel name="overview">
        <ProductOverview />
      </CarrotTabPanel>
      <CarrotTabPanel name="specs">
        <ProductSpecs />
      </CarrotTabPanel>
      <CarrotTabPanel name="reviews">
        <ProductReviews />
      </CarrotTabPanel>
      <CarrotTabPanel name="shipping">
        <ShippingInfo />
      </CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Settings Tabs Controlled by Route Query

vue
<script setup lang="ts">
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
  CarrotTabs, CarrotTabList, CarrotTab,
  CarrotTabPanels, CarrotTabPanel,
} from "@carrot/design-vue-tabs";

const route = useRoute();
const router = useRouter();

const activeTab = computed({
  get: () => (route.query.tab as string) || "profile",
  set: (tab) => router.replace({ query: { ...route.query, tab } }),
});
</script>

<template>
  <CarrotTabs v-model="activeTab">
    <CarrotTabList variant="pills">
      <CarrotTab name="profile">Profile</CarrotTab>
      <CarrotTab name="security">Security</CarrotTab>
      <CarrotTab name="notifications">Notifications</CarrotTab>
      <CarrotTab name="billing">Billing</CarrotTab>
    </CarrotTabList>

    <CarrotTabPanels>
      <CarrotTabPanel name="profile"><ProfileSettings /></CarrotTabPanel>
      <CarrotTabPanel name="security"><SecuritySettings /></CarrotTabPanel>
      <CarrotTabPanel name="notifications"><NotificationSettings /></CarrotTabPanel>
      <CarrotTabPanel name="billing"><BillingSettings /></CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Tabs with Icons and Notification Badges

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  CarrotTabs, CarrotTabList, CarrotTab,
  CarrotTabPanels, CarrotTabPanel,
} from "@carrot/design-vue-tabs";
import InboxIcon from "./icons/InboxIcon.vue";
import AlertIcon from "./icons/AlertIcon.vue";

const unread = ref(5);
</script>

<template>
  <CarrotTabs default-value="inbox">
    <CarrotTabList>
      <CarrotTab name="inbox">
        <template #icon><InboxIcon /></template>
        Inbox
        <template #badge>
          <span v-if="unread > 0" class="badge-count">{{ unread }}</span>
        </template>
      </CarrotTab>
      <CarrotTab name="alerts">
        <template #icon><AlertIcon /></template>
        Alerts
      </CarrotTab>
      <CarrotTab name="archived" disabled>Archived</CarrotTab>
    </CarrotTabList>

    <CarrotTabPanels>
      <CarrotTabPanel name="inbox"><InboxList /></CarrotTabPanel>
      <CarrotTabPanel name="alerts"><AlertsList /></CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Common Patterns

Unmount Strategy for Multi-Step Forms

Use unmount strategy so each step resets when the user navigates away:

vue
<template>
  <CarrotTabs v-model="step" mount-strategy="unmount">
    <CarrotTabList>
      <CarrotTab name="personal">Personal</CarrotTab>
      <CarrotTab name="address">Address</CarrotTab>
      <CarrotTab name="review">Review</CarrotTab>
    </CarrotTabList>
    <CarrotTabPanels>
      <CarrotTabPanel name="personal"><PersonalForm @done="step = 'address'" /></CarrotTabPanel>
      <CarrotTabPanel name="address"><AddressForm @done="step = 'review'" /></CarrotTabPanel>
      <CarrotTabPanel name="review"><ReviewAndSubmit /></CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Eager Mount for SEO Content

vue
<template>
  <!-- All panels rendered to DOM immediately for search engine indexing -->
  <CarrotTabs default-value="tab1" mount-strategy="eager">
    <CarrotTabList>
      <CarrotTab name="tab1">Content A</CarrotTab>
      <CarrotTab name="tab2">Content B</CarrotTab>
    </CarrotTabList>
    <CarrotTabPanels>
      <CarrotTabPanel name="tab1"><SeoContent1 /></CarrotTabPanel>
      <CarrotTabPanel name="tab2"><SeoContent2 /></CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Scrollable Tab List for Many Tabs

vue
<template>
  <CarrotTabs default-value="jan">
    <CarrotTabList scrollable aria-label="Months">
      <CarrotTab v-for="month in months" :key="month.id" :name="month.id">
        {{ month.label }}
      </CarrotTab>
    </CarrotTabList>
    <CarrotTabPanels>
      <CarrotTabPanel v-for="month in months" :key="month.id" :name="month.id">
        <MonthReport :month="month.id" />
      </CarrotTabPanel>
    </CarrotTabPanels>
  </CarrotTabs>
</template>

Carrot