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)
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:
import { Tab, TabPanel } from '@bpmlib/vue-satabs';Types:
import type { TabItem } from '@bpmlib/vue-satabs';Peer Dependencies
Library ini memerlukan peer dependencies berikut:
Wajib:
npm install vue@^3.3.0 tailwindcss@^4.0.0Opsional (untuk icons):
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| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.3.0 | Required | Vue 3 framework |
tailwindcss | ^4.0.0 | Required | Untuk styling lengkap |
@fortawesome/fontawesome-svg-core | ^6.0.0 || ^7.0.0 | Optional | FontAwesome core library |
@fortawesome/free-solid-svg-icons | ^6.0.0 || ^7.0.0 | Optional | Icon pack untuk tab icons |
@fortawesome/vue-fontawesome | ^3.0.0 | Optional | Vue wrapper untuk FontAwesome |
Catatan: Library menyediakan minimal CSS (animations only). Tailwind CSS wajib untuk styling lengkap.
Instalasi
Package Installation
npm install @bpmlib/vue-satabsyarn add @bpmlib/vue-satabspnpm add @bpmlib/vue-satabsbun install @bpmlib/vue-satabsVue Plugin Installation
Register components globally via plugin:
// 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:
<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:
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
</script>Import
Basic Import:
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import type { TabItem } from '@bpmlib/vue-satabs';CSS Import:
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:
<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
TabPanelcorresponds dengan item di arraytabs(by index) v-modeluntuk 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:
<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
valueuntuk URL-friendly identifiers query-paramenables URL synchronization@changeevent fires when tab changesTabPanelvalue 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
TabPanelyang diproses - Elemen non-TabPanel (div, p, custom components) diabaikan secara diam-diam
- Tidak ada error yang dilempar, tidak ada warning yang ditampilkan
Example:
<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:
<!-- ❌ 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:
<!-- ✅ 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:
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 number1 - 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:
// ✅ 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:
// 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-modelberhasil 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:
activeTab.value = 'premium'; // v-model updates
// But: Panel menampilkan empty state, user can't click the buttonSolusi: Validasi sebelum selection programmatic:
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;
}
}
};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:
<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.
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 denganvalueyang sama akan dicocokkan - Matching terjadi otomatis - tidak perlu konfigurasi manual
Default Matching (by index):
<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):
<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-parampada 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:
<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:
<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 tabrole="tab"pada setiap buttonrole="tabpanel"pada setiap panelaria-selecteduntuk status activearia-controlsmenghubungkan tabs ke panelsaria-labelledbymenghubungkan panels ke tabsaria-orientationuntuk 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)
<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):
<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):
<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 akhirtrue - 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
| Name | Type | Default | Description |
|---|---|---|---|
tabs | TabItem[] | - | Array konfigurasi tab yang mendefinisikan label, icon, visibility |
queryParam | string | undefined | Nama URL query parameter untuk sinkronisasi active tab dengan URL |
vertical | boolean | false | Render tabs secara vertikal alih-alih horizontal |
hideTabCondition | (tab: TabItem) => boolean | undefined | Fungsi untuk menyembunyikan tabs secara dinamis berdasarkan kondisi runtime. Lihat selengkapnya |
disableTabCondition | (tab: TabItem) => boolean | undefined | Fungsi untuk menonaktifkan tabs secara dinamis berdasarkan kondisi runtime. Lihat selengkapnya |
alt | boolean | false | Terapkan tema styling alternatif (menggunakan warna ternary alih-alih primary) |
hideTabCondition
Fungsi dipanggil untuk setiap tab pada setiap render untuk menentukan visibility.
Signature:
(tab: TabItem) => booleanParameters:
| Name | Type | Description |
|---|---|---|
tab | TabItem | Object 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:
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:
(tab: TabItem) => booleanParameters:
| Name | Type | Description |
|---|---|---|
tab | TabItem | Object 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:
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
| Name | Type | Default | Description |
|---|---|---|---|
v-model | string | number | 0 | Active tab value (atau index jika TabItem tidak punya custom value) |
Events
| Name | Payload | Description |
|---|---|---|
change | string | number | Dipicu 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
valueproperty (if defined) - Tab's array index (if no custom value)
Timing:
- Fires setelah
v-modeldiupdate - Fires after internal state changes
- tidak dipicu pada mount awal (hanya pada interaksi user atau perubahan programmatic)
Example:
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:
<Tab
v-model="activeTab"
:tabs="tabs"
@change="handleChange"
>
<TabPanel>...</TabPanel>
</Tab>vs watching v-model:
// 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 kejelasanSlots
| Name | Props | Description |
|---|---|---|
default | - | Place TabPanel components here, satu per TabItem |
Contoh:
<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
| Name | Type | Default | Description |
|---|---|---|---|
value | string | number | undefined | Custom identifier untuk mencocokkan dengan TabItem.value (opsional, default cocokkan berdasarkan index) |
Slots
| Name | Props | Description |
|---|---|---|
default | - | Panel content yang akan lazy loaded |
Contoh:
<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.
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
name: stringTeks label tab yang ditampilkan pada button.
Contoh:
const tabs: TabItem[] = [
{ name: 'Profile' },
{ name: 'Settings' },
];icon
icon?: anyFontAwesome icon object (e.g., faHome, faUser) untuk ditampilkan pada button tab. Requires @fortawesome/vue-fontawesome peer dependency.
Contoh:
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
disabled?: boolean
// Default: falseIf true, tab terlihat tapi tidak clickable (berwarna abu-abu/nonaktif). User tidak bisa select tab ini via click atau keyboard.
Contoh:
const tabs: TabItem[] = [
{ name: 'Public', disabled: false },
{ name: 'Premium Feature', disabled: !user.isPremium },
];Catatan: Disabled tabs dilewati dalam keyboard navigation.
hide
hide?: boolean
// Default: falseIf true, tab completely hidden dari UI (tidak rendered sama sekali).
Contoh:
const tabs: TabItem[] = [
{ name: 'Public' },
{ name: 'Admin Only', hide: !user.isAdmin },
];Perbedaan dengan disabled:
hide: true→ Tab tidak muncul di UIdisabled: true→ Tab muncul tapi berwarna abu-abu/nonaktif
value
value?: string | number
// Default: undefined (uses index)Custom identifier untuk tab, used for:
- URL query params (URL-friendly identifiers)
- v-model binding (semantic values instead of indices)
- TabPanel matching (explicit pairing)
Contoh:
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
- 2. Dynamic Tab Control (Hide + Disable Conditions)
- 3. Programmatic Tab Selection dengan v-model
1. URL Synchronization dengan Custom Values
Sync active tab dengan URL query parameter menggunakan semantic values.
Tab Definition:
const activeTab = ref('overview');
const tabs: TabItem[] = [
{ name: 'Overview', value: 'overview' },
{ name: 'Features', value: 'features' },
{ name: 'Pricing', value: 'pricing' },
];Template:
<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=pricinginstead 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:
// 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:
// 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:
<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:
hideTabConditioncompletely removes tabs dari UIdisableTabConditionshows 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:
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:
<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-modelprovides two-way binding untuk active tab- Bisa set
activeTab.valuedirectly untuk programmatic navigation - Perfect untuk multi-step forms, wizards, atau guided workflows
- Combine dengan
disableTabConditionuntuk prevent skipping steps
Styling
CSS yang Disediakan
@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
| Class | Description |
|---|---|
.sa-tab-group | Root container untuk entire tab system |
.sa-tab-horizontal | Modifier untuk horizontal layout |
.sa-tab-vertical | Modifier untuk vertical layout |
.tab-list | Container untuk tab buttons |
.tab-list-horizontal | Horizontal tab list layout |
.tab-list-vertical | Vertical tab list layout |
Tab Button Classes
| Class | Description |
|---|---|
.tab-button | Base styling untuk tab button |
.tab-button.active | Active/selected tab state (default: orange/primary theme) |
.tab-button.active.alt | Active state dengan alternative theme (reddish-purple/ternary) |
.tab-button.nonactive | Inactive tab state dengan hover effects |
.tab-button.tab-disabled | Disabled tab state (berwarna abu-abu/nonaktif, tidak bisa diklik) |
Tab Panel Classes
| Class | Description |
|---|---|
.sa-tab-panels | Container untuk panel content area |
.tab-panel-wrapper | Wrapper untuk transition animation |
.sa-tab-panel | Individual panel content styling |
.tab-panel-empty | Empty state when no valid tab selected |
Links
- Repository: https://repo.ppsdmmigas.id/bpmlib/js/vue-satabs
- Registry: https://js.pkg.ppsdmmigas.id