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:

  • tabTabItem 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 .valueselectedIndex.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 */
}