Skip to content

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

PropTypeDefaultDescription
modelValuestringRequired. The id of the currently active tab.
tabsTabOption[][]Array of tab objects, each with at least id and name.
showSeparatorbooleantrueShow a vertical divider line between tab buttons.

Events

EventPayloadDescription
update:modelValuestringEmitted when a tab is clicked. Supports v-model.
changestringEmitted 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 NameScope PropsDescription
[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

  1. Use unique id values — Each tab's id must be unique within the array. The id is used as the v-model value and the slot name.
  2. Keep tab counts reasonable — The component renders horizontally. Use 2–6 tabs for the best UX. For more items, consider a different navigation pattern.
  3. Leverage the change event for side effects — Use @change for actions like fetching data. Use v-model for reactive state binding.
  4. 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-model is bound to a ref variable.
  • Verify that the modelValue matches one of the tab id values exactly.

Custom slot not rendering

  • The slot name must match the tab's id exactly (case-sensitive).
  • Example: If the tab id is "report", the slot must be #report.

Separator not visible

  • The separator is a 1px gray vertical line. It only appears when showSeparator is true (the default) and there are 2 or more tabs.