Appearance
Tab Component
A lightweight, accessible tab navigation component with v-model support. Renders a horizontal tab bar with active-state styling, optional separators between tabs, and named slots for fully custom tab content.
Table of Contents
Import
ts
import { Tab } from "dolphin-components";
import type { TabOption, TabProps } from "dolphin-components";Basic Usage
vue
<template>
<Tab v-model="activeTab" :tabs="tabs" />
<div v-if="activeTab === 'overview'">Overview content</div>
<div v-if="activeTab === 'details'">Details content</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "dolphin-components";
const activeTab = ref("overview");
const tabs = [
{ id: "overview", name: "Overview" },
{ id: "details", name: "Details" },
];
</script>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | — | Required. The id of the currently active tab. |
tabs | TabOption[] | [] | Array of tab objects, each with at least id and name. |
showSeparator | boolean | true | Show a vertical divider line between tab buttons. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Emitted when a tab is clicked. Supports v-model. |
change | string | Emitted alongside update:modelValue when a tab is clicked. Useful for side-effect handlers. |
Slots
The component provides dynamic named slots based on each tab's id. This allows you to fully customize the label content rendered inside the tab button.
| Slot Name | Scope Props | Description |
|---|---|---|
[tab.id] | { tab: TabOption, active: boolean } | Custom content for the tab button with that id. Falls back to tab.name text if the slot is not provided. |
Example with custom slot:
vue
<Tab v-model="activeTab" :tabs="tabs">
<template #report="{ tab, active }">
<span class="flex items-center gap-1">
<Icons name="FileText" :size="14" />
{{ tab.name }}
</span>
</template>
</Tab>TypeScript Interfaces
typescript
interface TabOption {
id: string; // Unique identifier for the tab
name: string; // Display label for the tab
[key: string]: any; // Extra properties (passed through slot scope)
}
interface TabProps {
modelValue: string; // Currently active tab id
tabs: TabOption[]; // Array of tab definitions
showSeparator?: boolean; // Show divider between tabs (default: true)
}Usage Examples
Without Separators
vue
<template>
<Tab v-model="activeTab" :tabs="tabs" :showSeparator="false" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "dolphin-components";
const activeTab = ref("general");
const tabs = [
{ id: "general", name: "General" },
{ id: "security", name: "Security" },
{ id: "notifications", name: "Notifications" },
];
</script>With Custom Slot Content
Render icons, badges, or any custom markup inside the tab button using the dynamic named slot.
vue
<template>
<Tab v-model="activeTab" :tabs="tabs">
<template #users="{ tab, active }">
<span class="flex items-center gap-1.5">
<Icons name="Users" :size="14" />
{{ tab.name }}
<span
class="bg-blue-100 text-blue-700 text-[11px] px-1.5 rounded-full"
>
{{ tab.count }}
</span>
</span>
</template>
<template #settings="{ active }">
<span class="flex items-center gap-1">
<Icons name="Settings" :size="14" />
Settings
</span>
</template>
</Tab>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Tab, Icons } from "dolphin-components";
import type { TabOption } from "dolphin-components";
const activeTab = ref("users");
const tabs: TabOption[] = [
{ id: "users", name: "Users", count: 42 },
{ id: "roles", name: "Roles" },
{ id: "settings", name: "Settings" },
];
</script>Listening to Tab Changes
vue
<template>
<Tab v-model="activeTab" :tabs="tabs" @change="onTabChange" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "dolphin-components";
const activeTab = ref("report");
const tabs = [
{ id: "report", name: "Report" },
{ id: "print", name: "Print" },
];
const onTabChange = (tabId: string) => {
console.log("Switched to tab:", tabId);
// Fetch data, update state, etc.
};
</script>Best Practices
- Use unique
idvalues — Each tab'sidmust be unique within the array. Theidis used as thev-modelvalue and the slot name. - Keep tab counts reasonable — The component renders horizontally. Use 2–6 tabs for the best UX. For more items, consider a different navigation pattern.
- Leverage the
changeevent for side effects — Use@changefor actions like fetching data. Usev-modelfor reactive state binding. - Use named slots sparingly — The default text rendering is clean and consistent. Only use custom slots when you need icons, badges, or other rich content.
Troubleshooting
Tab does not highlight when clicked
- Ensure
v-modelis bound to arefvariable. - Verify that the
modelValuematches one of the tabidvalues exactly.
Custom slot not rendering
- The slot name must match the tab's
idexactly (case-sensitive). - Example: If the tab
idis"report", the slot must be#report.
Separator not visible
- The separator is a 1px gray vertical line. It only appears when
showSeparatoristrue(the default) and there are 2 or more tabs.