SaTabs (Tab dan TabPanel)
Vue 3 tab component dengan lazy rendering, URL synchronization, dan custom field mapping.
Versi: 0.1.10 Changelog
Kategori: UI Component (Vue 3)
WARNING
MVP LIBRARY Library ini di-extract dari family project. Styling tightly coupled dengan parent project dan mengharuskan class definitions tertentu. Lihat Styling untuk detail.
TL;DR
Vue 3 tab component dengan lazy rendering dan URL synchronization. Support v-model, keyboard navigation, conditional show/hide, dan custom field mapping untuk array data apapun.
Components:
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';Composable (advanced):
import { useTabs } from '@bpmlib/vue-satabs';Installation & Setup
Requirements
Peer Dependencies
Wajib:
npm install vue@^3.3.0 tailwindcss@^4.0.0Opsional (untuk icons):
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome| Dependency | Versi | Status |
|---|---|---|
vue | ^3.3.0 | Required |
tailwindcss | ^4.0.0 | Required |
@fortawesome/vue-fontawesome | ^3.0.0 | Optional |
Package Installation
npm install @bpmlib/vue-satabsyarn add @bpmlib/vue-satabspnpm add @bpmlib/vue-satabsImport
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';Plugin Setup
Opsional — untuk registrasi global sehingga Tab dan TabPanel tersedia di seluruh aplikasi tanpa import:
// main.ts
import { createApp } from 'vue';
import VueSatabs from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';
createApp(App).use(VueSatabs).mount('#app');Quick Start
Basic Usage
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';
const tabs = [
{ name: 'Overview', value: 'overview' },
{ name: 'Settings', value: 'settings' },
];
const activeTab = ref('overview');
</script>
<template>
<Tab :tabs="tabs" v-model="activeTab">
<TabPanel value="overview">Konten Overview</TabPanel>
<TabPanel value="settings">Konten Settings</TabPanel>
</Tab>
</template>Key Points:
tabsmendefinisikan daftar tab — fieldnamewajibTabPaneldicocokkan ke tab via propvalueyang sama denganTabItem.valuev-modeltrack dan kontrol tab yang aktif
Core Concepts
Lazy Rendering
Panel konten tidak di-render sampai pertama kali tab diaktifkan. Setelah diaktifkan, panel tetap di-render meski berpindah tab (tidak di-destroy). Cocok untuk komponen berat atau tab dengan data fetch.
Tab Value & Panel Matching
Setiap tab diidentifikasi via field value di TabItem. TabPanel dicocokkan ke tab via prop value yang sama:
<Tab :tabs="[{ name: 'Overview', value: 'overview' }]">
<TabPanel value="overview">Konten</TabPanel>
</Tab>Jika value tidak di-set di TabItem maupun TabPanel, matching berdasarkan posisi (index 0 → panel pertama, dst).
URL Synchronization
Prop queryParam menyinkronkan tab aktif ke URL query parameter — tab bisa di-share via URL dan dipulihkan saat reload.
queryParam menerima string | boolean:
trueatau""→ gunakan key default'tab'- String → gunakan string tersebut sebagai key
- Tidak di-set → tidak ada URL sync
<!-- URL: ?tab=settings -->
<Tab :tabs="tabs" query-param>
<!-- URL: ?view=settings -->
<Tab :tabs="tabs" query-param="view">Field Mapping
TIP
NEW v0.1.9
Prop fieldMap memungkinkan penggunaan array dengan field name apapun tanpa transform manual. Component bersifat generic atas T — IDE akan autocomplete nilai fieldMap dari key tipe array tabs.
<script setup>
// Field 'label' dan 'slug', bukan 'name' dan 'value'
const categories = [{ label: 'Beranda', slug: 'home' }];
</script>
<template>
<!-- IDE autocomplete: 'label', 'slug' -->
<Tab :tabs="categories" :field-map="{ name: 'label', value: 'slug' }">
<TabPanel value="home">Konten</TabPanel>
</Tab>
</template>Hanya sebutkan field yang berbeda dari default (name, value, icon, disabled, hide).
API Reference
Components
Tab
Komponen container utama. Tempatkan TabPanel di default slot.
Props
| Name | Type | Default | Description |
|---|---|---|---|
tabs | T[] | - | Array tab (required). Gunakan TabItem[] atau object lain dengan fieldMap |
fieldMap | TabFieldMap<T> | - | Map field object ke field TabItem Lihat selengkapnya |
queryParam | string | boolean | - | Sync tab aktif ke URL query Lihat selengkapnya |
v-model | string | number | 0 | Value tab yang aktif |
vertical | boolean | false | Layout vertikal |
alt | boolean | false | Style alternatif (warna ternary) |
hideTabCondition | (tab: TabItem) => boolean | - | Sembunyikan tab secara dinamis Lihat selengkapnya |
disableTabCondition | (tab: TabItem) => boolean | - | Disable tab secara dinamis Lihat selengkapnya |
pulsingBadge | boolean | false | Aktifkan animasi ping pada semua badge tab |
fieldMap
Map field object milik user ke field yang diharapkan TabItem. Hanya sebutkan field yang berbeda dari nama default.
Structure:
interface TabFieldMap<T> {
name?: keyof T & string; // Default field: 'name'
icon?: keyof T & string; // Default field: 'icon'
disabled?: keyof T & string; // Default field: 'disabled'
hide?: keyof T & string; // Default field: 'hide'
value?: keyof T & string; // Default field: 'value'
badge?: keyof T & string; // Default field: 'badge'
}Contoh:
<Tab :tabs="products" :field-map="{ name: 'title', value: 'sku', disabled: 'isLocked' }">Use Case: Pakai ketika data dari API memiliki field name berbeda dari default TabItem, sehingga tidak perlu transform sebelum di-pass ke Tab.
queryParam
Sinkronisasi tab aktif ke URL query parameter.
Values:
trueatau""→ key'tab'(contoh:?tab=overview)- String → key custom (contoh:
query-param="page"→?page=overview) - Tidak di-set → tidak ada URL sync
Contoh:
<Tab :tabs="tabs" query-param />
<Tab :tabs="tabs" query-param="section" />hideTabCondition
Function per tab untuk menentukan apakah tab disembunyikan. Tab tersembunyi tidak render di UI sama sekali.
Signature:
hideTabCondition: (tab: TabItem) => booleanContoh:
const hideTabCondition = (tab) => tab.value === 'admin' && !user.isAdmin;Use Case: Visibility berbasis role atau permission yang ditentukan saat runtime.
disableTabCondition
Function per tab untuk menentukan apakah tab di-disable. Tab di-disable tetap tampil tapi tidak bisa diklik.
Signature:
disableTabCondition: (tab: TabItem) => booleanContoh:
const disableTabCondition = (tab) => tab.value === 'premium' && !user.hasPremium;Use Case: Menunjukkan tab ada tapi belum accessible (misalnya fitur locked atau perlu upgrade).
Model
| Name | Type | Default | Description |
|---|---|---|---|
v-model | string | number | 0 | Value tab yang aktif saat ini |
Events
| Name | Payload | Description |
|---|---|---|
change | string | number | Di-emit setiap kali tab berpindah |
Slots
| Name | Props | Description |
|---|---|---|
default | - | Tempatkan komponen TabPanel di sini |
button | { tab: TabItem, index: number } | Kustomisasi konten tombol tab Lihat selengkapnya |
button
TIP
NEW v0.1.10
Menggantikan konten default di dalam setiap <button> tab (icon + label) dengan markup kustom. Button element itu sendiri — termasuk semua class, ARIA attribute, dan keyboard handler — tetap dikelola oleh library.
Scope:
tab—TabItemdari tab yang sedang di-renderindex— posisi tombol (0-based, urutan dari arraytabs)
Contoh:
<Tab :tabs="tabs">
<template #button="{ tab, index }">
<img :src="icons[tab.value]" class="w-4 h-4" />
<span>{{ tab.name }}</span>
<span v-if="tab.disabled" class="text-xs opacity-60">🔒</span>
</template>
<TabPanel value="a">...</TabPanel>
</Tab>Catatan: Jika slot tidak di-pass, fallback ke rendering default (icon FontAwesome + label).
Accessibility
- Keyboard navigation:
ArrowLeft/ArrowRight(horizontal),ArrowUp/ArrowDown(vertical),Home,End - ARIA:
role="tablist",role="tab",role="tabpanel",aria-selected,aria-controls,aria-orientation
TabPanel
Wrapper konten untuk satu panel tab. Harus digunakan sebagai child langsung dari Tab.
Cara Penggunaan:
TabPanel dicocokkan ke tab via prop value yang sama dengan TabItem.value. Jika keduanya tidak punya value, matching berdasarkan posisi.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string | number | index | Identifier panel — harus sama dengan TabItem.value |
Slots
| Name | Props | Description |
|---|---|---|
default | - | Konten panel tab |
Contoh:
<Tab :tabs="tabs">
<TabPanel value="a">Panel A</TabPanel>
<TabPanel value="b">Panel B</TabPanel>
</Tab>Composables
useTabs
NOTE
Composable internal yang digunakan oleh Tab. Export ini tersedia untuk use case advanced — misalnya membangun custom tab container.
Core state management untuk tabs: selection, lazy loading, URL sync, dan arah animasi.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
tabs | Ref<TabItem[]> | - | Reactive array tab |
queryParam | string | - | Key URL query parameter |
hideTabCondition | (tab: TabItem) => boolean | - | Filter hide per tab |
disableTabCondition | (tab: TabItem) => boolean | - | Filter disable per tab |
Returns
| Name | Type | Description |
|---|---|---|
selectedIndex | Ref<string | number> | Value tab yang aktif |
visibleTabs | ComputedRef<ProcessedTabItem[]> | Tab yang tidak hidden |
direction | ComputedRef<'left' | 'right'> | Arah slide animation |
selectTab | (value: string | number) => void | Pindah ke tab berdasarkan value |
isLoaded | (index: number) => boolean | Cek apakah panel sudah pernah di-render |
registerPanel | (index: number) => void | Registrasi panel saat mount |
⚠️ Penting — Penggunaan .value:
- Di
<script>: Perlu.value→selectedIndex.value - Di
<template>: Tidak perlu.value(auto-unwrap)
Types
TabItem
interface TabItem {
name: string;
icon?: any;
disabled?: boolean;
hide?: boolean;
value?: string | number;
badge?: boolean | string;
}Konfigurasi satu tab.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | - | Label teks tab (required) |
icon | any | - | FontAwesome icon object |
disabled | boolean | false | Tab tampil tapi tidak bisa diklik |
hide | boolean | false | Sembunyikan tab dari UI sepenuhnya |
value | string | number | - | Identifier unik — digunakan v-model, URL sync, dan panel matching |
badge | boolean | string | - | Notification badge: true = dot, string = pill dengan teks |
TabFieldMap
TIP
NEW v0.1.9
interface TabFieldMap<T = Record<string, any>> {
name?: keyof T & string;
icon?: keyof T & string;
disabled?: keyof T & string;
hide?: keyof T & string;
value?: keyof T & string;
badge?: keyof T & string;
}Map field dari object milik user ke field yang diharapkan TabItem. Ketika digunakan di Tab, IDE autocomplete nilai dari tipe array tabs.
Examples
Contains:
- 1. Basic Usage
- 2. v-model & @change
- 3. URL Query Sync
- 4. Vertical Layout & Alt Style
- 5. Conditional Tabs (Hide & Disable)
- 6. Custom Field Mapping ⭐ NEW v0.1.9
- 7. Dengan Icons
- 8. Plugin Registration
- 9. Dynamic Panels dengan v-for ⭐ NEW v0.1.10
- 10. Notification Badge ⭐ NEW v0.1.10
- 11. Custom Button Slot ⭐ NEW v0.1.10
1. Basic Usage
Paling sederhana — tab pertama aktif secara default.
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';
const tabs = [
{ name: 'Beranda', value: 'home' },
{ name: 'Profil', value: 'profile' },
];
</script>
<template>
<Tab :tabs="tabs">
<TabPanel value="home">Konten Beranda</TabPanel>
<TabPanel value="profile">Konten Profil</TabPanel>
</Tab>
</template>2. v-model & @change
Kontrol tab aktif secara programmatic dan react terhadap perubahan.
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const tabs = [
{ name: 'Data', value: 'data' },
{ name: 'Preview', value: 'preview' },
];
const activeTab = ref('data');
</script>
<template>
<Tab
:tabs="tabs"
v-model="activeTab"
@change="(val) => console.log('Tab aktif:', val)"
>
<TabPanel value="data">Form input</TabPanel>
<TabPanel value="preview">Preview hasil</TabPanel>
</Tab>
<button @click="activeTab = 'preview'">Lihat Preview</button>
</template>3. URL Query Sync
Tab aktif tersimpan di URL — bisa di-share dan survive page reload.
<template>
<!-- URL: ?tab=settings -->
<Tab :tabs="tabs" query-param>
<TabPanel value="overview">Overview</TabPanel>
<TabPanel value="settings">Settings</TabPanel>
</Tab>
<!-- URL: ?section=settings -->
<Tab :tabs="tabs" query-param="section">
<TabPanel value="overview">Overview</TabPanel>
<TabPanel value="settings">Settings</TabPanel>
</Tab>
</template>4. Vertical Layout & Alt Style
<template>
<!-- Vertikal -->
<Tab :tabs="tabs" vertical>
<TabPanel value="a">Panel A</TabPanel>
<TabPanel value="b">Panel B</TabPanel>
</Tab>
<!-- Alt style (warna ternary) -->
<Tab :tabs="tabs" alt>
<TabPanel value="a">Panel A</TabPanel>
<TabPanel value="b">Panel B</TabPanel>
</Tab>
</template>5. Conditional Tabs (Hide & Disable)
hide menyembunyikan sepenuhnya; disabled tampil tapi terkunci.
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const user = { role: 'editor', hasPremium: false };
const tabs = [
{ name: 'Konten', value: 'content' },
{ name: 'Admin', value: 'admin' },
{ name: 'Premium', value: 'premium' },
];
</script>
<template>
<Tab
:tabs="tabs"
:hide-tab-condition="(tab) => tab.value === 'admin' && user.role !== 'admin'"
:disable-tab-condition="(tab) => tab.value === 'premium' && !user.hasPremium"
>
<TabPanel value="content">Editor konten</TabPanel>
<TabPanel value="admin">Panel admin</TabPanel>
<TabPanel value="premium">Fitur premium</TabPanel>
</Tab>
</template>Key Points:
- Alternatif: set
hide: trueataudisabled: truelangsung di objectTabItem - Kondisi runtime (per-render) → gunakan
hideTabCondition/disableTabCondition - Kondisi statis → cukup di field
TabItem
6. Custom Field Mapping
TIP
NEW v0.1.9
Gunakan data dari API langsung tanpa transform manual. IDE autocomplete key dari tipe array.
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const categories = ref([
{ label: 'Semua', slug: 'all' },
{ label: 'Aktif', slug: 'active', locked: true },
]);
</script>
<template>
<!-- IDE menyarankan: 'label', 'slug', 'locked' -->
<Tab
:tabs="categories"
:field-map="{ name: 'label', value: 'slug', disabled: 'locked' }"
>
<TabPanel value="all">Semua item</TabPanel>
<TabPanel value="active">Item aktif</TabPanel>
</Tab>
</template>7. Dengan Icons
Memerlukan @fortawesome/vue-fontawesome sebagai peer dependency.
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
import { faHouse, faGear } from '@fortawesome/free-solid-svg-icons';
const tabs = [
{ name: 'Beranda', value: 'home', icon: faHouse },
{ name: 'Pengaturan', value: 'settings', icon: faGear },
];
</script>
<template>
<Tab :tabs="tabs">
<TabPanel value="home">Konten Beranda</TabPanel>
<TabPanel value="settings">Konten Pengaturan</TabPanel>
</Tab>
</template>Catatan: Jika @fortawesome/vue-fontawesome tidak terinstall, hanya label teks yang tampil — tidak ada error.
8. Plugin Registration
Setelah app.use(), Tab dan TabPanel tersedia di seluruh aplikasi tanpa import.
// main.ts
import { createApp } from 'vue';
import VueSatabs from '@bpmlib/vue-satabs';
import '@bpmlib/vue-satabs/style.css';
import App from './App.vue';
createApp(App).use(VueSatabs).mount('#app');<!-- AnyComponent.vue — tidak perlu import Tab/TabPanel -->
<template>
<Tab :tabs="tabs">
<TabPanel value="a">Panel A</TabPanel>
</Tab>
</template>9. Dynamic Panels dengan v-for
TIP
NEW v0.1.10
TabPanel dapat di-render secara dinamis menggunakan v-for. Kamu bisa mix panel statis dan dinamis dalam satu slot.
<script setup lang="ts">
import { ref } from 'vue';
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const tabs = ref([
{ name: 'Semua', value: 'all' },
{ name: 'Level 1', value: 'l1' },
{ name: 'Level 2', value: 'l2' },
]);
const levels = ref([
{ value: 'l1', label: 'Level 1' },
{ value: 'l2', label: 'Level 2' },
]);
</script>
<template>
<Tab :tabs="tabs">
<TabPanel value="all">Semua data</TabPanel>
<TabPanel v-for="lvl in levels" :key="lvl.value" :value="lvl.value">
Data {{ lvl.label }}
</TabPanel>
</Tab>
</template>Key Points:
- Selalu set
:keydan:valueyang unik padaTabPaneldi dalamv-for - Panel statis dan dinamis bisa dikombinasikan dalam satu
Tab
10. Notification Badge
TIP
NEW v0.1.10
badge: true menampilkan dot; string menampilkan pill. pulsingBadge mengaktifkan animasi ping pada semua badge secara bersamaan.
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const tabs = [
{ name: 'Inbox', value: 'inbox', badge: '3' },
{ name: 'Alerts', value: 'alerts', badge: true },
{ name: 'Archive', value: 'archive' },
];
</script>
<template>
<Tab :tabs="tabs" :pulsing-badge="true">
<TabPanel value="inbox">Pesan masuk</TabPanel>
<TabPanel value="alerts">Notifikasi</TabPanel>
<TabPanel value="archive">Arsip</TabPanel>
</Tab>
</template>Key Points:
badge: true→ dot merah kecil di pojok kanan atas tombolbadge: '3'→ pill merah dengan teks3- Override warna: set
--tab-badge-bgpada ancestor element Lihat selengkapnya
11. Custom Button Slot
TIP
NEW v0.1.10
Slot #button menggantikan konten dalam tombol tab. Button element (ARIA, keyboard, class) tetap dikelola library.
<script setup lang="ts">
import { Tab, TabPanel } from '@bpmlib/vue-satabs';
const tabs = [
{ name: 'Overview', value: 'overview', badge: true },
{ name: 'Settings', value: 'settings' },
];
</script>
<template>
<Tab :tabs="tabs">
<template #button="{ tab, index }">
<span class="font-bold text-sm">{{ index + 1 }}.</span>
<span>{{ tab.name }}</span>
<span v-if="tab.badge" class="ml-auto w-2 h-2 rounded-full bg-red-400" />
</template>
<TabPanel value="overview">Overview</TabPanel>
<TabPanel value="settings">Settings</TabPanel>
</Tab>
</template>Key Points:
tabadalahTabItem— aksesname,value,badge,disabled, dllindexadalah posisi tombol (0-based)- Ketika
#buttondi-pass, built-in badge daritab.badgetidak otomatis dirender — kamu kelola sendiri jika butuh
Styling
CSS Import
import '@bpmlib/vue-satabs/style.css';Disediakan:
- Slide transition (kiri/kanan) antar panel
- Visibility control untuk lazy rendering
TIDAK Disediakan:
- Warna, spacing, typography
- Tampilan button tab
- Layout structure
WARNING
Styling terbatas. Component memerlukan class definitions dari parent project untuk tampil dengan benar.
Expected Classes dari Parent Project
Component menggunakan class names berikut yang harus didefinisikan di parent project:
Tab Container
.sa-tab-group— root wrapper.sa-tab-horizontal/.sa-tab-vertical— layout modifier.sa-tab-panels— container panel
Tab Buttons
.tab-list— container tombol tab.tab-list-horizontal/.tab-list-vertical— layout.tab-button— tombol tab individual.active— state tab aktif.nonactive— state tab tidak aktif.tab-disabled— state tab di-disable.alt— modifier style alternatif
Tab Panel
.sa-tab-panel— wrapper konten panel.tab-panel-wrapper— inner wrapper untuk transition.tab-panel-empty— state kosong (tidak ada panel valid)
TIP
NON-FAMILY PROJECT Jika digunakan di luar family project, definisikan class-class di atas dengan styling project kamu. Component hanya butuh class names ada — tidak peduli implementasinya.
WARNING
PENTING Tanpa CSS class definitions ini, component akan render tanpa styling. Minimal definisikan .tab-button, .active, dan .sa-tab-panel.
CSS Variables
TIP
NEW v0.1.10
Component menggunakan CSS custom properties berikut yang bisa di-override dari host:
| Variable | Default | Description |
|---|---|---|
--tab-badge-bg | #ef4444 (red-500) | Background color notification badge |
--tab-badge-color | #fff | Text color notification badge (pill mode) |
Contoh override:
/* Override per instance Tab */
.my-tab-container {
--tab-badge-bg: #8b5cf6; /* purple-500 */
}
/* Override global */
:root {
--tab-badge-bg: #f97316; /* orange-500 */
}Links
- Repository: Gitea
- Registry: Internal NPM