Skip to content

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)

npm versionTypeScriptVueTailwind CSS

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:

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

Composable (advanced):

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

Installation & Setup ​

Requirements ​

Peer Dependencies ​

Wajib:

bash
npm install vue@^3.3.0 tailwindcss@^4.0.0

Opsional (untuk icons):

bash
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome
DependencyVersiStatus
vue^3.3.0Required
tailwindcss^4.0.0Required
@fortawesome/vue-fontawesome^3.0.0Optional

Package Installation ​

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

Import ​

ts
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:

ts
// 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 ​

vue
<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:

  • tabs mendefinisikan daftar tab — field name wajib
  • TabPanel dicocokkan ke tab via prop value yang sama dengan TabItem.value
  • v-model track 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:

vue
<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:

  • true atau "" → gunakan key default 'tab'
  • String → gunakan string tersebut sebagai key
  • Tidak di-set → tidak ada URL sync
vue
<!-- 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.

vue
<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

NameTypeDefaultDescription
tabsT[]-Array tab (required). Gunakan TabItem[] atau object lain dengan fieldMap
fieldMapTabFieldMap<T>-Map field object ke field TabItem Lihat selengkapnya
queryParamstring | boolean-Sync tab aktif ke URL query Lihat selengkapnya
v-modelstring | number0Value tab yang aktif
verticalbooleanfalseLayout vertikal
altbooleanfalseStyle alternatif (warna ternary)
hideTabCondition(tab: TabItem) => boolean-Sembunyikan tab secara dinamis Lihat selengkapnya
disableTabCondition(tab: TabItem) => boolean-Disable tab secara dinamis Lihat selengkapnya
pulsingBadgebooleanfalseAktifkan 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:

ts
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:

vue
<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:

  • true atau "" → key 'tab' (contoh: ?tab=overview)
  • String → key custom (contoh: query-param="page" → ?page=overview)
  • Tidak di-set → tidak ada URL sync

Contoh:

vue
<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:

ts
hideTabCondition: (tab: TabItem) => boolean

Contoh:

ts
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:

ts
disableTabCondition: (tab: TabItem) => boolean

Contoh:

ts
const disableTabCondition = (tab) => tab.value === 'premium' && !user.hasPremium;

Use Case: Menunjukkan tab ada tapi belum accessible (misalnya fitur locked atau perlu upgrade).

Model

NameTypeDefaultDescription
v-modelstring | number0Value tab yang aktif saat ini

Events

NamePayloadDescription
changestring | numberDi-emit setiap kali tab berpindah

Slots

NamePropsDescription
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 — TabItem dari tab yang sedang di-render
  • index — posisi tombol (0-based, urutan dari array tabs)

Contoh:

vue
<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

NameTypeDefaultDescription
valuestring | numberindexIdentifier panel — harus sama dengan TabItem.value

Slots

NamePropsDescription
default-Konten panel tab

Contoh:

vue
<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

NameTypeDefaultDescription
tabsRef<TabItem[]>-Reactive array tab
queryParamstring-Key URL query parameter
hideTabCondition(tab: TabItem) => boolean-Filter hide per tab
disableTabCondition(tab: TabItem) => boolean-Filter disable per tab

Returns

NameTypeDescription
selectedIndexRef<string | number>Value tab yang aktif
visibleTabsComputedRef<ProcessedTabItem[]>Tab yang tidak hidden
directionComputedRef<'left' | 'right'>Arah slide animation
selectTab(value: string | number) => voidPindah ke tab berdasarkan value
isLoaded(index: number) => booleanCek apakah panel sudah pernah di-render
registerPanel(index: number) => voidRegistrasi panel saat mount

⚠️ Penting — Penggunaan .value:

  • Di <script>: Perlu .value → selectedIndex.value
  • Di <template>: Tidak perlu .value (auto-unwrap)

Types ​

TabItem ​

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

Konfigurasi satu tab.

FieldTypeDefaultDescription
namestring-Label teks tab (required)
iconany-FontAwesome icon object
disabledbooleanfalseTab tampil tapi tidak bisa diklik
hidebooleanfalseSembunyikan tab dari UI sepenuhnya
valuestring | number-Identifier unik — digunakan v-model, URL sync, dan panel matching
badgeboolean | string-Notification badge: true = dot, string = pill dengan teks

TabFieldMap ​

TIP

NEW v0.1.9

ts
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 ​

Paling sederhana — tab pertama aktif secara default.

vue
<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.

vue
<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.

vue
<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 ​

vue
<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.

vue
<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: true atau disabled: true langsung di object TabItem
  • 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.

vue
<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.

vue
<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.

ts
// 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');
vue
<!-- 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.

vue
<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 :key dan :value yang unik pada TabPanel di dalam v-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.

vue
<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 tombol
  • badge: '3' → pill merah dengan teks 3
  • Override warna: set --tab-badge-bg pada 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.

vue
<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:

  • tab adalah TabItem — akses name, value, badge, disabled, dll
  • index adalah posisi tombol (0-based)
  • Ketika #button di-pass, built-in badge dari tab.badge tidak otomatis dirender — kamu kelola sendiri jika butuh

Styling ​

CSS Import ​

ts
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:

VariableDefaultDescription
--tab-badge-bg#ef4444 (red-500)Background color notification badge
--tab-badge-color#fffText color notification badge (pill mode)

Contoh override:

css
/* Override per instance Tab */
.my-tab-container {
  --tab-badge-bg: #8b5cf6; /* purple-500 */
}

/* Override global */
:root {
  --tab-badge-bg: #f97316; /* orange-500 */
}