Skip to content

vue-satabs

SuperApp-optimized Vue 3 tab component dengan lazy rendering, keyboard navigation, dan URL synchronization

Versi: 0.1.7
Kategori: UI Component (Vue 3)

npm versionTypeScriptVueTailwind CSS

MVP LIBRARY

Library ini diekstrak dari family project. Styling terikat erat dengan parent project dan mengharuskan class definitions tertentu. Lihat Styling untuk detail.


TL;DR

Apa yang library ini lakukan

Vue 3 tab component dengan lazy rendering, keyboard navigation, dan URL synchronization. Menyediakan accessible tabs dengan minimal styling - kamu kontrol penuh tampilan dengan Tailwind CSS atau custom styles. Optimized untuk SuperApp architecture dengan dynamic tab visibility/disable conditions.

Components:

ts
import { Tab, TabPanel } from '@bpmlib/vue-satabs';

Types:

ts
import type { TabItem } from '@bpmlib/vue-satabs';

Peer Dependencies

Library ini memerlukan peer dependencies berikut:

Wajib:

bash
npm install vue@^3.3.0 tailwindcss@^4.0.0

Opsional (untuk icons):

bash
npm install @fortawesome/fontawesome-svg-core@^6.0.0
npm install @fortawesome/free-solid-svg-icons@^6.0.0
npm install @fortawesome/vue-fontawesome@^3.0.0
DependencyVersiStatusDeskripsi
vue^3.3.0RequiredVue 3 framework
tailwindcss^4.0.0RequiredUntuk styling lengkap
@fortawesome/fontawesome-svg-core^6.0.0 || ^7.0.0OptionalFontAwesome core library
@fortawesome/free-solid-svg-icons^6.0.0 || ^7.0.0OptionalIcon pack untuk tab icons
@fortawesome/vue-fontawesome^3.0.0OptionalVue wrapper untuk FontAwesome

Catatan: Library menyediakan minimal CSS (animations only). Tailwind CSS wajib untuk styling lengkap.


Instalasi

Package Installation

bash
npm install @bpmlib/vue-satabs
bash
yarn add @bpmlib/vue-satabs
bash
pnpm add @bpmlib/vue-satabs
bash
bun install @bpmlib/vue-satabs

Vue Plugin Installation

Register components globally via plugin:

ts
// main.ts
import { createApp } from 'vue';
import VueSatabs from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';

const app = createApp(App);

// Register plugin
app.use(VueSatabs);

app.mount('#app');

Global Component Registration

Setelah plugin installation, components available globally:

vue
<template>
  <!-- No import needed -->
  <Tab :tabs="tabs">
    <TabPanel>Content 1</TabPanel>
    <TabPanel>Content 2</TabPanel>
  </Tab>
</template>

Catatan: Tanpa plugin, import components manually di setiap file:

vue
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
</script>

Import

Basic Import:

ts
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import type { TabItem } from '@bpmlib/vue-satabs';

CSS Import:

ts
import '@bpmlib/vue-satabs/style.css';

Requirements

Runtime

Node.js:

  • Minimum: 18.0.0
  • Recommended: 20.0.0+

TypeScript:

  • Minimum: 5.0.0
  • Configuration: "moduleResolution": "bundler"

Browser Compatibility:

  • Modern browsers (ES2020+)
  • Chrome 90+, Firefox 88+, Safari 14+, Edge 90+

Quick Start

Basic Usage

Contoh paling sederhana tanpa icons:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import type { TabItem } from '@bpmlib/vue-satabs';

const activeTab = ref(0);

const tabs: TabItem[] = [
  { name: 'Profile' },
  { name: 'Settings' },
  { name: 'Notifications' },
];
</script>

<template>
  <Tab v-model="activeTab" :tabs="tabs">
    <TabPanel>
      <h2>Profile Content</h2>
      <p>Your profile information here.</p>
    </TabPanel>
    
    <TabPanel>
      <h2>Settings Content</h2>
      <p>Your settings here.</p>
    </TabPanel>
    
    <TabPanel>
      <h2>Notifications Content</h2>
      <p>Your notifications here.</p>
    </TabPanel>
  </Tab>
</template>

Key Points:

  • Setiap TabPanel corresponds dengan item di array tabs (by index)
  • v-model untuk two-way binding active tab
  • Konten lazy loaded (hanya dirender saat pertama kali dipilih)

Catatan: Untuk vertical layout, tambahkan prop vertical pada Tab component.

Comprehensive Example with Icons

Full-featured example dengan FontAwesome icons:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import type { TabItem } from '@bpmlib/vue-satabs';
import { faUser, faCog, faBell, faLock } from '@fortawesome/free-solid-svg-icons';

const activeTab = ref('profile');

const tabs: TabItem[] = [
  { 
    name: 'Profile', 
    icon: faUser,
    value: 'profile'
  },
  { 
    name: 'Settings', 
    icon: faCog,
    value: 'settings'
  },
  { 
    name: 'Notifications', 
    icon: faBell,
    value: 'notifications',
    disabled: false
  },
  { 
    name: 'Security', 
    icon: faLock,
    value: 'security',
    hide: false
  },
];

const handleTabChange = (value: string | number) => {
  console.log('Tab changed to:', value);
};
</script>

<template>
  <Tab 
    v-model="activeTab" 
    :tabs="tabs"
    query-param="tab"
    @change="handleTabChange"
  >
    <TabPanel value="profile">
      <h2>Profile Content</h2>
      <p>Manage your profile.</p>
    </TabPanel>
    
    <TabPanel value="settings">
      <h2>Settings Content</h2>
      <p>Configure your preferences.</p>
    </TabPanel>
    
    <TabPanel value="notifications">
      <h2>Notifications Content</h2>
      <p>View your notifications.</p>
    </TabPanel>
    
    <TabPanel value="security">
      <h2>Security Content</h2>
      <p>Manage security settings.</p>
    </TabPanel>
  </Tab>
</template>

Key Points:

  • Icons require FontAwesome peer dependencies
  • Custom value untuk URL-friendly identifiers
  • query-param enables URL synchronization
  • @change event fires when tab changes
  • TabPanel value harus match dengan TabItem value

Core Concepts

Brief intro tentang fundamental mechanics dari library ini yang perlu dipahami users.

Tab-Panel Pairing

Fitur yang selalu diterapkan: Hanya komponen TabPanel yang diproses dalam slot Tab - elemen lain diabaikan. Tab dan Panel harus match secara ketat berdasarkan jumlah dan urutan.

Cara kerja:

  • Komponen Tab secara otomatis memfilter konten slot
  • Hanya VNodes yang type-nya TabPanel yang diproses
  • Elemen non-TabPanel (div, p, custom components) diabaikan secara diam-diam
  • Tidak ada error yang dilempar, tidak ada warning yang ditampilkan

Example:

vue
<Tab :tabs="tabs">
  <TabPanel>Panel 1</TabPanel>           <!-- ✅ Processed -->
  <div>Some wrapper</div>                 <!-- ❌ Ignored -->
  <CustomComponent />                     <!-- ❌ Ignored -->
  <TabPanel>Panel 2</TabPanel>           <!-- ✅ Processed -->
  <p>Random text</p>                      <!-- ❌ Ignored -->
  <TabPanel>Panel 3</TabPanel>           <!-- ✅ Processed -->
</Tab>

Result:

  • Hanya 3 TabPanels yang diproses (dicocokkan dengan tabs[0], tabs[1], tabs[2])
  • Semua elemen lain tidak dirender sama sekali
  • Index TabPanel tetap sequential: 0, 1, 2 (bukan 0, 3, 5)

Rules:

  • HARUS gunakan TabPanel - Tidak bisa langsung taruh content tanpa wrapper
  • Satu TabPanel per tab - Jumlah TabPanel harus match dengan jumlah TabItem
  • Tidak boleh nested - TabPanel harus direct children dari Tab
  • Konsistensi penggunaan value - Semua tabs punya value, atau semua gunakan index

Wrong Usage:

vue
<!-- ❌ Content without TabPanel wrapper -->
<Tab :tabs="tabs">
  <div>This won't render</div>
</Tab>

<!-- ❌ Wrapped in other component -->
<Tab :tabs="tabs">
  <CustomWrapper>
    <TabPanel>This won't work</TabPanel>
  </CustomWrapper>
</Tab>

<!-- ❌ Mismatch count: 3 tabs but 2 panels -->
<Tab :tabs="[...3 tabs]">
  <TabPanel>Panel 1</TabPanel>
  <TabPanel>Panel 2</TabPanel>
  <!-- Missing Panel 3 -->
</Tab>

Correct Usage:

vue
<!-- ✅ Direct TabPanel children, count matches -->
<Tab :tabs="[...3 tabs]">
  <TabPanel>Panel 1</TabPanel>
  <TabPanel>Panel 2</TabPanel>
  <TabPanel>Panel 3</TabPanel>
</Tab>

Edge Case: Mixed Value Types

AVOID NUMBER/STRING COLLISION

Jangan gunakan angka atau stringified number sebagai custom value!

Problem:

ts
const tabs = [
  { name: 'Tab 1', value: 'x' },     // value: 'x'
  { name: 'Tab 2' },                  // fallback: 1 (number)
  { name: 'Tab 3', value: '1' },     // value: '1' (string)
  { name: 'Tab 4', value: 1 },       // value: 1 (number)
];

Issues:

  • Tab 2 (no value) → Internal value: 1 (number from index)
  • Tab 3 (value: '1') → Type confusion: string '1' vs number 1
  • Tab 4 (value: 1) → COLLISION with Tab 2's fallback value!
  • URL Conflict: Tab 2 generates ?tab=2 (index+1), Tab 3 has ?tab=1

What happens:

  • v-model="1" (number) → Selects Tab 2 (fallback)
  • v-model="'1'" (string) → Selects Tab 3 (custom value)
  • v-model="1" with Tab 4 → Ambiguous! (Tab 2 or Tab 4?)

Solution: Use semantic string values untuk semua tabs:

ts
// ✅ Good: Semantic strings
const tabs = [
  { name: 'Tab 1', value: 'home' },
  { name: 'Tab 2', value: 'profile' },
  { name: 'Tab 3', value: 'settings' },
];

// ✅ Good: All use index (no custom values)
const tabs = [
  { name: 'Tab 1' },  // uses index 0
  { name: 'Tab 2' },  // uses index 1
  { name: 'Tab 3' },  // uses index 2
];

// ❌ Bad: Mixed values and indices
const tabs = [
  { name: 'Tab 1', value: 'x' },
  { name: 'Tab 2' },              // fallback to index
  { name: 'Tab 3', value: 'y' },
];

// ❌ Bad: Number values (collision risk)
const tabs = [
  { name: 'Tab 1', value: 1 },
  { name: 'Tab 2' },              // fallback to 1 (COLLISION!)
];

Programmatic Control: Tab Tersembunyi/Nonaktif

Perilaku saat mencoba select tab yang tersembunyi/nonaktif secara programmatic:

ts
// Setup
const activeTab = ref('admin');

const tabs = [
  { name: 'Dashboard', value: 'dashboard' },
  { name: 'Admin', value: 'admin', hide: true },  // Hidden
  { name: 'Premium', value: 'premium', disabled: true },  // Disabled
];

// Try to select hidden tab
activeTab.value = 'admin';

Apa yang terjadi:

  • v-model berhasil diupdate (selectedIndex menjadi 'admin')
  • ❌ Panel TIDAK dirender (menampilkan empty state)
  • ❌ Tab button tidak terlihat di UI (tersembunyi)
  • ⚠️ Tidak ada automatic fallback ke tab lain
  • ⚠️ Tidak ada error - silent failure

Perilaku sama untuk tab yang nonaktif:

ts
activeTab.value = 'premium';  // v-model updates
// But: Panel menampilkan empty state, user can't click the button

Solusi: Validasi sebelum selection programmatic:

ts
const goToTab = (tabValue: string) => {
  const tab = tabs.find(t => t.value === tabValue);
  
  // Cek apakah accessible
  if (tab && !tab.hide && !tab.disabled) {
    activeTab.value = tabValue;
  } else {
    console.warn('Tab tidak accessible:', tabValue);
    // Fallback ke first available tab
    const firstAvailable = tabs.find(t => !t.hide && !t.disabled);
    if (firstAvailable) {
      activeTab.value = firstAvailable.value ?? 0;
    }
  }
};

See: Tab Component - Slots


Lazy Rendering

Fitur yang selalu diterapkan: Tab panels hanya dirender saat pertama kali dipilih.

Cara kerja:

  • Konten panel tidak dirender sampai tab pertama kali diklik
  • Setelah dirender, panel tetap disimpan di DOM (tidak dihapus saat switch tab)
  • Menghemat waktu render awal dan memory untuk tabs dengan konten berat

Example:

vue
<Tab v-model="activeTab" :tabs="tabs">
  <TabPanel>
    <!-- Rendered immediately (first tab selected by default) -->
    <ExpensiveComponent />
  </TabPanel>
  
  <TabPanel>
    <!-- NOT rendered until user clicks this tab -->
    <HeavyDataTable />
  </TabPanel>
</Tab>

Benefit: Aplikasi load lebih cepat karena tidak perlu render semua tab content di awal.

See: Tab Component - Props


Automatic Tab Matching

Fitur yang selalu diterapkan: Library otomatis mencocokkan TabPanel dengan TabItem berdasarkan index atau custom value.

Cara kerja:

  • By Index (default): TabPanel ke-N cocok dengan TabItem index N (0-indexed)
  • By Value (custom): Jika TabItem punya property value, TabPanel dengan value yang sama akan dicocokkan
  • Matching terjadi otomatis - tidak perlu konfigurasi manual

Default Matching (by index):

vue
<script setup lang="ts">
const tabs = [
  { name: 'Tab 1' }, // index 0
  { name: 'Tab 2' }, // index 1
];
</script>

<template>
  <Tab :tabs="tabs">
    <TabPanel> <!-- Matches index 0 --> </TabPanel>
    <TabPanel> <!-- Matches index 1 --> </TabPanel>
  </Tab>
</template>

Custom Matching (by value):

vue
<script setup lang="ts">
const tabs = [
  { name: 'Profile', value: 'user-profile' },
  { name: 'Settings', value: 'user-settings' },
];
</script>

<template>
  <Tab :tabs="tabs">
    <TabPanel value="user-profile"> <!-- Matches by value --> </TabPanel>
    <TabPanel value="user-settings"> <!-- Matches by value --> </TabPanel>
  </Tab>
</template>

Rule: Jumlah TabPanel harus sama dengan jumlah TabItem (non-hidden).

Catatan: Jika active tab disembunyikan, panel akan show empty state (Tidak otomatis beralih ke tab lain).

See: TabItem Type, Example 2


URL Synchronization

Fitur opsional: Sinkronkan active tab dengan browser URL query parameter.

Cara kerja:

  • Set prop query-param pada komponen Tab
  • Active tab akan disinkronkan dengan URL query parameter
  • User bisa bookmark/share URL dengan tab tertentu yang terpilih
  • Tombol browser back/forward bekerja dengan navigasi tab

Format URL:

  • Dengan custom values: ?tab=profile (menggunakan TabItem.value)
  • Tanpa custom values: ?tab=1 (menggunakan index + 1, 1-indexed agar URL user-friendly)

Example:

vue
<script setup lang="ts">
const tabs = [
  { name: 'Home', value: 'home' },
  { name: 'About', value: 'about' },
];
</script>

<template>
  <Tab :tabs="tabs" query-param="tab">
    <!-- URL: ?tab=home when first tab active -->
    <!-- URL: ?tab=about when second tab active -->
    <TabPanel value="home">...</TabPanel>
    <TabPanel value="about">...</TabPanel>
  </Tab>
</template>

Saat TIDAK menggunakan custom values:

vue
<script setup lang="ts">
const tabs = [
  { name: 'Home' }, // index 0
  { name: 'About' }, // index 1
];
</script>

<template>
  <Tab :tabs="tabs" query-param="section">
    <!-- URL: ?section=1 when first tab active (1-indexed) -->
    <!-- URL: ?section=2 when second tab active -->
    <TabPanel>...</TabPanel>
    <TabPanel>...</TabPanel>
  </Tab>
</template>

Catatan: URL updates via history.replaceState (tidak menambah browser history).

See: Example 2


Keyboard Navigation & Accessibility

Fitur yang selalu diterapkan: Navigasi keyboard lengkap dan atribut ARIA sudah built-in.

Dukungan Keyboard:

  • Arrow Left/Up: Tab sebelumnya (wraps ke terakhir)
  • Arrow Right/Down: Tab berikutnya (wraps ke pertama)
  • Home: Tab pertama
  • End: Tab terakhir
  • Tab: Focus elemen berikutnya (perilaku browser standar)

Atribut ARIA (otomatis):

  • role="tablist" pada container tab
  • role="tab" pada setiap button
  • role="tabpanel" pada setiap panel
  • aria-selected untuk status active
  • aria-controls menghubungkan tabs ke panels
  • aria-labelledby menghubungkan panels ke tabs
  • aria-orientation untuk layout vertical/horizontal

Manajemen Focus:

  • Hanya tab aktif yang tabbable (tabindex="0")
  • Tab tidak aktif memiliki tabindex="-1"
  • Navigasi keyboard otomatis memfokuskan button tab yang dipilih
  • Tab yang dinonaktifkan dilewati dalam navigasi keyboard

Dukungan Screen Reader:

  • Mengumumkan jumlah tab dan posisi
  • Mengumumkan perubahan status terpilih
  • Mengumumkan status nonaktif

Example: (tidak perlu konfigurasi apapun)

vue
<Tab :tabs="tabs">
  <!-- Keyboard navigation works automatically -->
  <!-- Screen readers announce properly -->
  <TabPanel>...</TabPanel>
</Tab>

Catatan: Semua accessibility features comply dengan WAI-ARIA Tab Panel pattern.


Dynamic Tab Visibility

Fitur opsional: Sembunyikan atau nonaktifkan tabs berdasarkan kondisi runtime.

Dua Pendekatan:

1. Static (via TabItem properties):

vue
<script setup lang="ts">
const tabs = [
  { name: 'Public', hide: false, disabled: false },
  { name: 'Admin Only', hide: !user.isAdmin },
  { name: 'Premium', disabled: !user.isPremium },
];
</script>

2. Dynamic (via condition functions):

vue
<script setup lang="ts">
const hideTabCondition = (tab: TabItem) => {
  // Return true to HIDE the tab
  return tab.name === 'Admin' && !user.isAdmin;
};

const disableTabCondition = (tab: TabItem) => {
  // Return true to DISABLE the tab
  return tab.name === 'Premium' && !user.isPremium;
};
</script>

<template>
  <Tab 
    :tabs="tabs"
    :hide-tab-condition="hideTabCondition"
    :disable-tab-condition="disableTabCondition"
  >
    <TabPanel>...</TabPanel>
  </Tab>
</template>

Perbedaan:

  • hide: Tab sepenuhnya dihapus dari UI
  • disabled: Tab terlihat tapi berwarna abu-abu/nonaktif dan tidak clickable

Logika Prioritas:

  • Static dan dynamic conditions combined dengan OR logic
  • Jika salah satu true, hasil akhir true
  • Example: hide = tab.hide || hideTabCondition(tab)

Perilaku saat active tab becomes inaccessible:

  • Nilai yang dipilih tetap sama
  • Panel menampilkan empty state (tidak ada content yang render)
  • Tidak otomatis beralih ke tab lain

See: Example 2


API Reference

Components

Tab

Main container component untuk tab navigation system.

Cara Penggunaan:

Wrap TabPanel components dalam Tab component. Jumlah TabPanel harus match dengan jumlah items di tabs array.

Accessibility:

Component ini sudah include built-in accessibility features:

  • Keyboard navigation: Arrow keys, Home, End untuk navigasi
  • ARIA attributes: Proper role, aria-selected, aria-controls
  • Screen reader support: Announces tab states dan position

Props

NameTypeDefaultDescription
tabsTabItem[]-Array konfigurasi tab yang mendefinisikan label, icon, visibility
queryParamstringundefinedNama URL query parameter untuk sinkronisasi active tab dengan URL
verticalbooleanfalseRender tabs secara vertikal alih-alih horizontal
hideTabCondition(tab: TabItem) => booleanundefinedFungsi untuk menyembunyikan tabs secara dinamis berdasarkan kondisi runtime. Lihat selengkapnya
disableTabCondition(tab: TabItem) => booleanundefinedFungsi untuk menonaktifkan tabs secara dinamis berdasarkan kondisi runtime. Lihat selengkapnya
altbooleanfalseTerapkan tema styling alternatif (menggunakan warna ternary alih-alih primary)

hideTabCondition

Fungsi dipanggil untuk setiap tab pada setiap render untuk menentukan visibility.

Signature:

ts
(tab: TabItem) => boolean

Parameters:

NameTypeDescription
tabTabItemObject tab saat ini dari array tabs

Returns: boolean

  • true → Tab akan disembunyikan (sepenuhnya dihapus dari UI)
  • false → Tab tetap terlihat

Called: Pada setiap render (reaktif terhadap perubahan state)

Example:

ts
const hideTabCondition = (tab: TabItem) => {
  // Hide admin tab if user is not admin
  if (tab.value === 'admin' && !user.isAdmin) {
    return true;
  }
  return false;
};

See: Core Concepts - Dynamic Tab Visibility, Example 2


disableTabCondition

Fungsi dipanggil untuk setiap tab pada setiap render untuk menentukan status nonaktif.

Signature:

ts
(tab: TabItem) => boolean

Parameters:

NameTypeDescription
tabTabItemObject tab saat ini dari array tabs

Returns: boolean

  • true → Tab akan dinonaktifkan (terlihat tapi berwarna abu-abu/nonaktif, tidak bisa diklik)
  • false → Tab tetap aktif

Called: Pada setiap render (reaktif terhadap perubahan state)

Example:

ts
const disableTabCondition = (tab: TabItem) => {
  // Disable premium features if not premium user
  if (tab.value === 'premium' && !user.isPremium) {
    return true;
  }
  return false;
};

See: Core Concepts - Dynamic Tab Visibility, Example 2


Model

NameTypeDefaultDescription
v-modelstring | number0Active tab value (atau index jika TabItem tidak punya custom value)

Events

NamePayloadDescription
changestring | numberDipicu saat active tab berubah, receives tab value. Lihat selengkapnya

change Event

Event Dipicu saat active tab berubah.

Payload Type: string | number

Payload Value:

  • Tab's value property (if defined)
  • Tab's array index (if no custom value)

Timing:

  • Fires setelah v-model diupdate
  • Fires after internal state changes
  • tidak dipicu pada mount awal (hanya pada interaksi user atau perubahan programmatic)

Example:

ts
const handleChange = (value: string | number) => {
  console.log('Active tab changed to:', value);
  
  // Track analytics
  trackEvent('tab_change', { tab: value });
  
  // Conditional logic
  if (value === 'settings') {
    loadSettings();
  }
};

Usage:

vue
<Tab 
  v-model="activeTab"
  :tabs="tabs"
  @change="handleChange"
>
  <TabPanel>...</TabPanel>
</Tab>

vs watching v-model:

ts
// Option 1: Use @change event
<Tab @change="handleChange" />

// Option 2: Watch v-model
watch(activeTab, (newValue) => {
  console.log('Tab changed:', newValue);
});

// Keduanya bekerja mirip, gunakan @change untuk kejelasan

Slots

NamePropsDescription
default-Place TabPanel components here, satu per TabItem

Contoh:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import type { TabItem } from '@bpmlib/vue-satabs';

const activeTab = ref(0);

const tabs: TabItem[] = [
  { name: 'Profile' },
  { name: 'Settings', disabled: false },
  { name: 'Admin', hide: !user.isAdmin },
];

const handleChange = (value: string | number) => {
  console.log('Tab changed:', value);
};
</script>

<template>
  <Tab 
    v-model="activeTab"
    :tabs="tabs"
    query-param="tab"
    @change="handleChange"
  >
    <TabPanel>Profile content</TabPanel>
    <TabPanel>Settings content</TabPanel>
    <TabPanel>Admin content</TabPanel>
  </Tab>
</template>

TabPanel

Wrapper component untuk individual tab content. Harus digunakan sebagai child dari Tab component.

Cara Penggunaan:

Place TabPanel di dalam Tab component slot. Konten lazy loaded (hanya dirender saat pertama kali dipilih).

Props

NameTypeDefaultDescription
valuestring | numberundefinedCustom identifier untuk mencocokkan dengan TabItem.value (opsional, default cocokkan berdasarkan index)

Slots

NamePropsDescription
default-Panel content yang akan lazy loaded

Contoh:

vue
<Tab :tabs="tabs">
  <!-- Match by index (default) -->
  <TabPanel>
    <div>Content untuk tab pertama</div>
  </TabPanel>
  
  <!-- cocokkan berdasarkan custom value -->
  <TabPanel value="custom-id">
    <div>Content dengan custom identifier</div>
  </TabPanel>
</Tab>

Catatan: TabPanel value harus match dengan TabItem value jika menggunakan custom values.


Types

TabItem

Configuration object untuk single tab item.

ts
interface TabItem {
  name: string;
  icon?: any;
  disabled?: boolean;
  hide?: boolean;
  value?: string | number;
}

Brief description: Type definition untuk konfigurasi individual tab, define label, icon, visibility, dan custom identifier.

Contains:

name
ts
name: string

Teks label tab yang ditampilkan pada button.

Contoh:

ts
const tabs: TabItem[] = [
  { name: 'Profile' },
  { name: 'Settings' },
];

icon
ts
icon?: any

FontAwesome icon object (e.g., faHome, faUser) untuk ditampilkan pada button tab. Requires @fortawesome/vue-fontawesome peer dependency.

Contoh:

ts
import { faUser, faCog } from '@fortawesome/free-solid-svg-icons';

const tabs: TabItem[] = [
  { name: 'Profile', icon: faUser },
  { name: 'Settings', icon: faCog },
];

Catatan: Icon optional - tabs bisa ditampilkan tanpa icon.


disabled
ts
disabled?: boolean
// Default: false

If true, tab terlihat tapi tidak clickable (berwarna abu-abu/nonaktif). User tidak bisa select tab ini via click atau keyboard.

Contoh:

ts
const tabs: TabItem[] = [
  { name: 'Public', disabled: false },
  { name: 'Premium Feature', disabled: !user.isPremium },
];

Catatan: Disabled tabs dilewati dalam keyboard navigation.


hide
ts
hide?: boolean
// Default: false

If true, tab completely hidden dari UI (tidak rendered sama sekali).

Contoh:

ts
const tabs: TabItem[] = [
  { name: 'Public' },
  { name: 'Admin Only', hide: !user.isAdmin },
];

Perbedaan dengan disabled:

  • hide: true → Tab tidak muncul di UI
  • disabled: true → Tab muncul tapi berwarna abu-abu/nonaktif

value
ts
value?: string | number
// Default: undefined (uses index)

Custom identifier untuk tab, used for:

  1. URL query params (URL-friendly identifiers)
  2. v-model binding (semantic values instead of indices)
  3. TabPanel matching (explicit pairing)

Contoh:

ts
const tabs: TabItem[] = [
  { name: 'Profile', value: 'user-profile' },
  { name: 'Settings', value: 'user-settings' },
];

// URL: ?tab=user-profile (instead of ?tab=1)
// v-model: 'user-profile' (instead of 0)

Catatan: Jika tidak diberikan, library menggunakan array index (0-indexed) sebagai value.

VALUE BEST PRACTICES

  • Gunakan semantic strings (e.g., 'profile', 'settings')
  • Avoid numbers atau stringified numbers (e.g., 1, '1') untuk prevent collision
  • Either semua tabs punya value, atau semua tanpa value

Lihat Core Concepts - Tab-Panel Pairing untuk detail edge cases.


Examples

Contains:

1. URL Synchronization dengan Custom Values

Sync active tab dengan URL query parameter menggunakan semantic values.

Tab Definition:

ts
const activeTab = ref('overview');

const tabs: TabItem[] = [
  { name: 'Overview', value: 'overview' },
  { name: 'Features', value: 'features' },
  { name: 'Pricing', value: 'pricing' },
];

Template:

vue
<Tab 
  v-model="activeTab"
  :tabs="tabs"
  query-param="section"
>
  <!-- URL: ?section=overview -->
  <TabPanel value="overview">
    <h2>Product Overview</h2>
  </TabPanel>
  
  <!-- URL: ?section=features -->
  <TabPanel value="features">
    <h2>Features</h2>
  </TabPanel>
  
  <!-- URL: ?section=pricing -->
  <TabPanel value="pricing">
    <h2>Pricing Plans</h2>
  </TabPanel>
</Tab>

Key Takeaways:

  • Custom values membuat URL human-readable: ?section=pricing instead of ?section=3
  • Users bisa bookmark atau share URL dengan specific tab selected
  • Initial tab loaded from URL on mount (jika query param exists)
  • URL updates via history.replaceState (tidak menambah history entries)

2. Dynamic Tab Control (Hide + Disable Conditions)

Control tab visibility dan availability based on runtime conditions seperti user permissions.

Setup:

ts
// Simulate user state
const user = ref({
  isAdmin: false,
  isPremium: false,
});

const tabs: TabItem[] = [
  { name: 'Dashboard', value: 'dashboard' },
  { name: 'Reports', value: 'reports' },
  { name: 'Admin Panel', value: 'admin' },
];

Condition Functions:

ts
// Hide tabs yang user tidak punya akses
const hideTabCondition = (tab: TabItem) => {
  if (tab.value === 'admin' && !user.value.isAdmin) {
    return true; // Tab completely hidden
  }
  return false;
};

// Disable tabs yang perlu upgrade
const disableTabCondition = (tab: TabItem) => {
  if (tab.value === 'reports' && !user.value.isPremium) {
    return true; // Tab visible but berwarna abu-abu/nonaktif
  }
  return false;
};

Template:

vue
<Tab 
  :tabs="tabs"
  :hide-tab-condition="hideTabCondition"
  :disable-tab-condition="disableTabCondition"
>
  <TabPanel value="dashboard">
    <h2>Dashboard (Always accessible)</h2>
  </TabPanel>
  
  <TabPanel value="reports">
    <h2>Reports (Disabled if not premium)</h2>
  </TabPanel>
  
  <TabPanel value="admin">
    <h2>Admin Panel (Hidden if not admin)</h2>
  </TabPanel>
</Tab>

Key Takeaways:

  • hideTabCondition completely removes tabs dari UI
  • disableTabCondition shows tabs tapi berwarna abu-abu/nonaktif dan tidak clickable
  • Both functions reactive - tabs update saat conditions change
  • Static dan dynamic conditions digabungkan dengan logika OR
  • Jika active tab becomes inaccessible, panel menampilkan empty state (tidak auto-switch)

3. Programmatic Tab Selection dengan v-model

Control active tab programmatically via v-model binding atau method calls.

Setup:

ts
const activeTab = ref('step1');

const tabs: TabItem[] = [
  { name: 'Step 1', value: 'step1' },
  { name: 'Step 2', value: 'step2' },
  { name: 'Step 3', value: 'step3' },
];

// Navigate to specific tab
const goToTab = (tabValue: string) => {
  activeTab.value = tabValue;
};

// Next/Previous navigation
const nextStep = () => {
  const currentIndex = tabs.findIndex(t => t.value === activeTab.value);
  if (currentIndex < tabs.length - 1) {
    activeTab.value = tabs[currentIndex + 1].value as string;
  }
};

const prevStep = () => {
  const currentIndex = tabs.findIndex(t => t.value === activeTab.value);
  if (currentIndex > 0) {
    activeTab.value = tabs[currentIndex - 1].value as string;
  }
};

Template:

vue
<div>
  <!-- Quick navigation buttons -->
  <button @click="goToTab('step1')">Jump to Step 1</button>
  <button @click="goToTab('step2')">Jump to Step 2</button>
  <button @click="goToTab('step3')">Jump to Step 3</button>
  
  <Tab v-model="activeTab" :tabs="tabs">
    <TabPanel value="step1">
      <h2>Step 1: Personal Info</h2>
      <button @click="nextStep">Continue to Step 2</button>
    </TabPanel>
    
    <TabPanel value="step2">
      <h2>Step 2: Address</h2>
      <button @click="prevStep">Back</button>
      <button @click="nextStep">Continue to Step 3</button>
    </TabPanel>
    
    <TabPanel value="step3">
      <h2>Step 3: Review</h2>
      <button @click="prevStep">Back</button>
      <button @click="submitForm">Submit</button>
    </TabPanel>
  </Tab>
</div>

Key Takeaways:

  • v-model provides two-way binding untuk active tab
  • Bisa set activeTab.value directly untuk programmatic navigation
  • Perfect untuk multi-step forms, wizards, atau guided workflows
  • Combine dengan disableTabCondition untuk prevent skipping steps

Styling

CSS yang Disediakan

css
@import '@bpmlib/vue-satabs/style.css';

Included:

  • Transition animations (slide-left, slide-right)
  • Basic animation timing (0.25s ease)

NOT Included:

  • Colors, spacing, typography
  • Layout structure
  • Component appearance (buttons, panels)
  • State styling (active, disabled, hover)

Catatan: Library provides minimal CSS - hanya animations. Semua visual styling harus disediakan oleh parent project.


Expected CSS Classes

Component mengharuskan parent project mendefinisikan classes berikut. Ini adalah classes yang library expect untuk exist - tanpa ini, tabs tidak akan styled properly.

Tab Container Classes

ClassDescription
.sa-tab-groupRoot container untuk entire tab system
.sa-tab-horizontalModifier untuk horizontal layout
.sa-tab-verticalModifier untuk vertical layout
.tab-listContainer untuk tab buttons
.tab-list-horizontalHorizontal tab list layout
.tab-list-verticalVertical tab list layout

Tab Button Classes

ClassDescription
.tab-buttonBase styling untuk tab button
.tab-button.activeActive/selected tab state (default: orange/primary theme)
.tab-button.active.altActive state dengan alternative theme (reddish-purple/ternary)
.tab-button.nonactiveInactive tab state dengan hover effects
.tab-button.tab-disabledDisabled tab state (berwarna abu-abu/nonaktif, tidak bisa diklik)

Tab Panel Classes

ClassDescription
.sa-tab-panelsContainer untuk panel content area
.tab-panel-wrapperWrapper untuk transition animation
.sa-tab-panelIndividual panel content styling
.tab-panel-emptyEmpty state when no valid tab selected