Appearance
@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
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Active tab name (v-model) |
defaultValue | string | first tab | Default tab when uncontrolled |
mountStrategy | TabMountStrategy | "lazy" | Panel mount/unmount behaviour |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Active tab changed |
change | string | Active 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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | TabsVariant | — | Visual style |
scrollable | boolean | false | Horizontal scroll on overflow |
full | boolean | false | Stretch tabs to fill width |
ariaLabel | string | — | Accessible 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
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique identifier (must match a panel) |
disabled | boolean | false | Disabled state |
badge | string | — | Badge text |
Slots
| Slot | Description |
|---|---|
default | Tab label |
icon | Leading icon |
badge | Trailing 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
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Must match a CarrotTab's name |
Mount Strategies
| Strategy | Behaviour | Use When |
|---|---|---|
lazy (default) | Mount on first activation, stay mounted | Most cases — preserves state, avoids upfront cost |
eager | All panels mounted immediately, toggled with v-show | SEO, or panels need to initialise in background |
unmount | Mount on activation, unmount on deactivation | Fresh state needed each time (e.g. forms that should reset) |
Keyboard Navigation
| Key | Action |
|---|---|
| ArrowRight / ArrowDown | Next tab (skips disabled, wraps) |
| ArrowLeft / ArrowUp | Previous tab (skips disabled, wraps) |
| Home | First enabled tab |
| End | Last enabled tab |
Focus follows selection — selecting a tab via keyboard also moves focus to it.
ARIA
The implementation follows the WAI-ARIA Tabs Pattern:
CarrotTabList→role="tablist"CarrotTab→role="tab",aria-selected,aria-controls, rovingtabindexCarrotTabPanel→role="tabpanel",aria-labelledby- IDs auto-generated with unique prefix per
CarrotTabsinstance
Dependencies
| Package | Purpose |
|---|---|
@carrot/design-components-tabs | CSS tokens + generated styles |
Build
bash
npm run buildBuilt 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>