vue3-router-tab
v1.4.6
Published
Vue 3 Router Tabs component: tabbed navigation for Vue Router with persistence, context menu, drag-and-drop reordering, theming, and reactive page-driven tab titles.
Downloads
716
Maintainers
Readme
Vue3 Router Tab
A powerful, feature-rich Vue 3 tab-bar plugin that keeps multiple routes alive with smooth transitions, context menus, drag-and-drop reordering, and optional cookie-based persistence. Built for modern Vue 3 applications with full TypeScript support.
⚠️ Breaking Change
The legacy contextmenu prop alias has been removed.
Use contextMenu instead.
✨ Key Features
- 🎯 Multi-tab Navigation - Keep multiple routes alive simultaneously with intelligent caching
- 🔄 7 Built-in Transitions - Smooth page transitions (swap, slide, fade, scale, flip, rotate, bounce)
- 🎨 Reactive Tab Titles - Automatically update tab titles, icons, and closability from component state
- 🖱️ Context Menu - Right-click tabs for refresh, close, and navigation options
- 🔀 Drag & Drop - Reorder tabs with drag-and-drop (sortable)
- 💾 Cookie Persistence - Restore tabs on page refresh with customizable options
- 🎭 Theme Support - Light, dark, and system themes with customizable colors
- ⚡ KeepAlive Support - Preserve component state when switching tabs with smart cache management
- ♿ Accessibility - Full WCAG compliance with ARIA labels, keyboard navigation, and screen reader support
- 🚀 Performance Optimized - Intelligent caching, memoization, and memory management
- 🎛️ Highly Configurable - Extensive props, events, and customization options
- 📱 TypeScript Support - Full TypeScript definitions with excellent developer experience
- 🔧 Error Recovery - Automatic error handling with graceful degradation and recovery mechanisms
📦 Installation
npm install vue3-router-tab
# or
pnpm add vue3-router-tab
# or
yarn add vue3-router-tab🚀 Quick Start
1. Register the Plugin
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import RouterTab from 'vue3-router-tab'
const app = createApp(App)
app.use(router)
app.use(RouterTab)
app.mount('#app')2. Basic Usage
<template>
<router-tab />
</template>That's it! You now have a fully functional tabbed router interface.
3. Enhanced Usage with Persistence
<template>
<router-tab
cookie-key="my-app-tabs"
:sortable="true"
:keep-alive="true"
/>
</template>📖 Usage Guide
Basic Configuration
<template>
<router-tab
cookie-key="app-tabs"
:keep-alive="true"
:max-alive="10"
:keep-last-tab="true"
:sortable="true"
page-transition="router-tab-fade"
tab-transition="router-tab-zoom"
/>
</template>Route Configuration
Configure your routes with tab metadata:
// router/index.ts
const routes = [
{
path: '/',
component: Home,
meta: {
title: 'Home',
icon: 'mdi-home',
keepAlive: true,
},
},
{
path: '/users',
component: Users,
meta: {
title: 'Users',
icon: 'mdi-account-group',
closable: true,
keepAlive: true,
},
},
{
path: '/settings',
component: Settings,
meta: {
title: 'Settings',
icon: 'mdi-cog',
closable: false, // Can't be closed
keepAlive: false,
},
},
]Reactive Tab Properties
Make your tabs dynamic by exposing reactive properties in your components:
<template>
<div>
<h1>{{ pageTitle }}</h1>
<button @click="updateTitle">Update Title</button>
<div v-if="loading">Loading...</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// These reactive properties automatically update the tab
const pageTitle = ref('Dashboard')
const loading = ref(false)
const notificationCount = ref(0)
// Tab title updates automatically
const routeTabTitle = computed(() => {
if (loading.value) return 'Loading...'
if (notificationCount.value > 0) return `Dashboard (${notificationCount.value})`
return pageTitle.value
})
// Tab icon changes based on state
const routeTabIcon = computed(() =>
loading.value ? 'mdi-loading mdi-spin' : 'mdi-view-dashboard'
)
// Prevent closing while loading
const routeTabClosable = computed(() => !loading.value)
function updateTitle() {
pageTitle.value = 'Updated Dashboard'
}
</script>🎨 Transitions
Choose from 7 built-in transition effects:
<!-- Default transition -->
<router-tab />
<!-- Custom transition -->
<router-tab page-transition="router-tab-scale" />
<!-- Advanced configuration -->
<router-tab :page-transition="{ name: 'router-tab-flip', mode: 'out-in' }" />Available Transitions
| Transition | Description | Best For |
|------------|-------------|----------|
| router-tab-swap | Smooth up/down slide with fade | General purpose (default) |
| router-tab-slide | Horizontal sliding | Dashboard navigation |
| router-tab-fade | Simple opacity fade | Minimal, subtle |
| router-tab-scale | Zoom in/out effect | Dramatic transitions |
| router-tab-flip | 3D flip animation | Modern, creative |
| router-tab-rotate | Rotation with scale | Playful, dynamic |
| router-tab-bounce | Elastic bounce | Fun, energetic |
🎭 Theming
Built-in Themes
import { setRouterTabsTheme } from 'vue3-router-tab'
// Switch themes at runtime
setRouterTabsTheme('dark')
setRouterTabsTheme('light')
setRouterTabsTheme('system') // Follows OS preferenceCustom Colors
import { setRouterTabsPrimary } from 'vue3-router-tab'
setRouterTabsPrimary({
primary: '#3b82f6',
background: '#ffffff',
text: '#1f2937',
activeBackground: '#3b82f6',
activeText: '#ffffff',
border: '#e5e7eb'
})CSS Customization
:root {
/* Layout */
--router-tab-header-height: 48px;
--router-tab-padding: 16px;
/* Colors */
--router-tab-primary: #3b82f6;
--router-tab-background: #ffffff;
--router-tab-active-background: #3b82f6;
}♿ Accessibility
Vue3 Router Tab is fully accessible with:
- ARIA Labels: Proper labeling for screen readers
- Keyboard Navigation: Arrow keys, Enter, Delete, Home, End
- Focus Management: Logical tab order and focus indicators
- Semantic HTML: Proper roles and structure
<!-- Accessible by default -->
<router-tab />
<!-- Custom ARIA labels -->
<router-tab aria-label="Main navigation tabs" />Keyboard Shortcuts
- Arrow Keys: Navigate between tabs
- Enter/Space: Activate selected tab
- Delete/Backspace: Close current tab (if closable)
- Home/End: Jump to first/last tab
🔧 API Reference
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| tabs | TabInput[] | [] | Initial tabs to display |
| keepAlive | boolean | true | Enable KeepAlive for tab components |
| maxAlive | number | 0 | Maximum cached components (0 = unlimited) |
| keepLastTab | boolean | true | Prevent closing the last tab |
| append | 'last' \| 'next' | 'last' | Position for new tabs |
| defaultPage | RouteLocationRaw | '/' | Default route |
| tabTransition | TransitionLike | 'router-tab-zoom' | Tab list transitions |
| pageTransition | TransitionLike | { name: 'router-tab-swap', mode: 'out-in' } | Page transitions |
| contextMenu | boolean \| RouterTabsMenuConfig[] | true | Context menu configuration |
| cookieKey | string | 'router-tabs:snapshot' | Persistence cookie key |
| persistence | RouterTabsPersistenceOptions \| null | null | Advanced persistence options |
| sortable | boolean | true | Enable drag-and-drop sorting |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| tab-sort | { tab: TabRecord, index: number } | Tab drag started |
| tab-sorted | { tab: TabRecord, fromIndex: number, toIndex: number } | Tab reordered |
Slots
| Slot | Props | Description |
|------|-------|-------------|
| start | - | Content before tab list |
| end | - | Content after tab list |
| default | { Component, route } | Custom page rendering |
🎛️ Programmatic API
Using the Composable
<script setup lang="ts">
import { useRouterTabs } from 'vue3-router-tab'
const tabs = useRouterTabs()
// Open a new tab
await tabs.openTab('/users')
// Close current tab
await tabs.closeTab()
// Refresh all tabs
await tabs.refreshAll()
</script>Controller Methods
interface RouterTabsContext {
// Navigation
openTab(to: RouteLocationRaw, replace?: boolean, refresh?: boolean | 'sameTab'): Promise<void>
closeTab(id?: string, options?: CloseTabOptions): Promise<void>
// Management
refreshTab(id?: string, force?: boolean): Promise<void>
refreshAll(force?: boolean): Promise<void>
removeTab(id: string, opts?: RemoveTabOptions): Promise<void>
// Cache Control
setTabAlive(id: string, alive: boolean): void
evictCache(id: string): void
clearCache(): void
getCacheKeys(): string[]
// State
reset(route?: RouteLocationRaw): Promise<void>
reload(): Promise<void>
// Utilities
getRouteKey(route: RouteLocationNormalizedLoaded | RouteLocationRaw): string
matchRoute(route: RouteLocationNormalizedLoaded | RouteLocationRaw): RouteMatchResult
// Persistence
snapshot(): RouterTabsSnapshot
hydrate(snapshot: RouterTabsSnapshot): Promise<void>
}🔄 Advanced Usage
Custom Context Menu
<router-tab
:contextMenu="[
'refresh',
'close',
{ id: 'duplicate', label: 'Duplicate Tab', handler: ({ target }) => openTab(target.to) },
{ id: 'closeOthers', label: 'Close All Others' },
{
id: 'openExternal',
label: 'Open in New Window',
handler: ({ target }) => window.open(target.to, '_blank')
}
]"
/>Custom Rendering
<router-tab>
<template #default="{ Component, route }">
<Suspense>
<ErrorBoundary>
<component :is="Component" :key="route.fullPath" />
</ErrorBoundary>
</Suspense>
</template>
</router-tab>Advanced Persistence
<router-tab
:persistence="{
cookieKey: 'my-app-tabs',
expiresInDays: 30,
path: '/',
secure: true,
sameSite: 'strict',
serialize: (snapshot) => encrypt(JSON.stringify(snapshot)),
deserialize: (data) => JSON.parse(decrypt(data))
}"
/>🧩 Composables
useReactiveTab
<script setup lang="ts">
import { useReactiveTab } from 'vue3-router-tab'
const { routeTabTitle, routeTabIcon, routeTabClosable } = useReactiveTab({
title: 'Dashboard',
icon: 'mdi-view-dashboard',
closable: true
})
</script>useLoadingTab
<script setup lang="ts">
import { useLoadingTab } from 'vue3-router-tab'
const isLoading = ref(false)
const { routeTabTitle, routeTabIcon, routeTabClosable } = useLoadingTab(isLoading, 'Dashboard')
</script>useNotificationTab
<script setup lang="ts">
import { useNotificationTab } from 'vue3-router-tab'
const notificationCount = ref(0)
const { routeTabTitle, routeTabIcon } = useNotificationTab(notificationCount, 'Messages')
</script>🎯 Examples
Complete App Example
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link>
<router-link to="/users">Users</router-link>
<router-link to="/settings">Settings</router-link>
</nav>
<router-tab
cookie-key="my-app"
:sortable="true"
page-transition="router-tab-fade"
@tab-sorted="onTabSorted"
/>
</div>
</template>
<script setup lang="ts">
import { useRouterTabs } from 'vue3-router-tab'
const tabs = useRouterTabs()
function onTabSorted({ tab, fromIndex, toIndex }) {
// Save order to backend
saveTabOrder(tab.id, toIndex)
}
</script>Real-time Updates
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouterTabs } from 'vue3-router-tab'
const tabs = useRouterTabs()
const messageCount = ref(0)
const isOnline = ref(true)
// Reactive tab title
const routeTabTitle = computed(() =>
messageCount.value > 0 ? `Messages (${messageCount.value})` : 'Messages'
)
// Reactive tab icon
const routeTabIcon = computed(() =>
isOnline.value ? 'mdi-message' : 'mdi-message-offline'
)
// Simulate real-time updates
onMounted(() => {
// Connect to WebSocket or polling service
connectToRealtimeUpdates((update) => {
if (update.type === 'message') {
messageCount.value++
} else if (update.type === 'status') {
isOnline.value = update.online
}
})
})
</script>🔍 Troubleshooting
Common Issues
Tabs not updating titles
- Ensure you're using
ref()orcomputed()for reactive properties - Check that properties are properly exposed in
<script setup>
Transitions not working
- Don't use custom
#defaultslot without transitions - Ensure transition names match available transitions
Persistence not working
- Check that cookies are enabled
- Verify
cookie-keyprop is set - Check browser console for cookie errors
KeepAlive not preserving state
- Ensure routes have
keepAlive: truein meta - Check that components have unique keys
TypeScript errors
import type { TabRecord, RouterTabsOptions } from 'vue3-router-tab'Performance Tips
- Use
maxAliveto limit cached components - Avoid deep watchers in tab components
- Use
evictCache()to manually clear cache when needed - Consider using
keepAlive: falsefor memory-intensive components
🌟 Migration Guide
From v1.x to v2.x
// Before
import RouterTab from 'vue3-router-tab'
// After (same)
import RouterTab from 'vue3-router-tab'The API is backward compatible. New features are additive.
Context Menu Prop Update
The legacy contextmenu prop alias has been removed.
Use:
<router-tab :contextMenu="true" />Instead of:
<router-tab :contextmenu="true" />📱 Browser Support
- Chrome: 90+
- Firefox: 88+
- Safari: 14+
- Edge: 90+
- Mobile: iOS Safari 14+, Chrome Mobile 90+
🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes
- Add tests if applicable
- Run the build:
npm run build - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
📄 License
MIT License - see the LICENSE file for details.
🙏 Acknowledgments
Built with ❤️ by the Vue.js community. Special thanks to all contributors and users.
Made with ❤️ for the Vue.js ecosystem
meta.key accepts the shortcuts fullPath, path, or name, or you can supply your own function.
Configuration
RouterTab Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| tabs | TabInput[] | [] | Initial tabs to display |
| keepAlive | boolean | true | Enable keep-alive for tab components |
| maxAlive | number | 0 | Maximum number of alive components (0 = unlimited) |
| keepLastTab | boolean | true | Prevent closing the last remaining tab |
| append | 'last' \| 'next' | 'last' | Where to append new tabs |
| defaultPage | string \| object | '/' | Default route to navigate to |
| tabTransition | string \| object | 'router-tab-zoom' | Transition for tab changes |
| pageTransition | string \| object | { name: 'router-tab-swap', mode: 'out-in' } | Transition for page changes |
| contextMenu | boolean \| array | true | Enable context menu or provide custom menu items |
| cookieKey | string | 'router-tabs:snapshot' | Cookie key for persistence |
| persistence | object \| null | null | Persistence configuration |
| sortable | boolean | true | Enable drag-and-drop tab sorting |
Page Transitions
Vue3 Router Tab includes 7 built-in page transition effects that are displayed when switching between tabs or refreshing pages.
Available Transitions
| Transition | Description | Use Case |
|------------|-------------|----------|
| router-tab-swap | Slides up/down with fade (default) | General purpose, smooth |
| router-tab-slide | Horizontal sliding animation | Dashboard-style navigation |
| router-tab-fade | Simple opacity fade | Subtle, minimal distraction |
| router-tab-scale | Zoom in/out effect | Dramatic, attention-grabbing |
| router-tab-flip | 3D flip animation | Creative, modern feel |
| router-tab-rotate | Rotation with scale | Playful, dynamic |
| router-tab-bounce | Elastic bounce effect | Fun, energetic |
Using Transitions
Default Transition
<router-tab />
<!-- Uses router-tab-swap by default -->Custom Transition
<router-tab
:page-transition="{ name: 'router-tab-scale', mode: 'out-in' }"
/>String Shorthand
<router-tab page-transition="router-tab-fade" />Dynamic Transitions
Change transitions at runtime:
<template>
<div>
<select v-model="currentTransition">
<option value="router-tab-swap">Swap</option>
<option value="router-tab-slide">Slide</option>
<option value="router-tab-fade">Fade</option>
<option value="router-tab-scale">Scale</option>
<option value="router-tab-flip">Flip</option>
<option value="router-tab-rotate">Rotate</option>
<option value="router-tab-bounce">Bounce</option>
</select>
<router-tab
:page-transition="{ name: currentTransition, mode: 'out-in' }"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTransition = ref('router-tab-swap')
</script>Custom Transitions
Create your own transitions by defining CSS classes:
/* Your custom transition */
.my-custom-enter-active,
.my-custom-leave-active {
transition: all 0.5s ease;
}
.my-custom-enter-from {
opacity: 0;
transform: translateX(100px);
}
.my-custom-leave-to {
opacity: 0;
transform: translateX(-100px);
}<router-tab page-transition="my-custom" />Transition Tips
- Performance: Use
mode: 'out-in'for smooth transitions without layout shifts - Duration: Built-in transitions are optimized at 0.5-0.8 seconds
- Accessibility: Consider users with motion sensitivity - provide options to disable
- Context: Match transition style to your app's design language
Changing Tab Titles Dynamically
Vue3 Router Tab automatically watches for reactive properties in your page components and updates the corresponding tab information in real-time.
Quick Start
Simply expose reactive properties from your component:
<template>
<div>
<h1>{{ routeTabTitle }}</h1>
<button @click="updateTitle">Change Tab Title</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// This ref is automatically watched - tab title updates when it changes!
const routeTabTitle = ref('My Page')
function updateTitle() {
routeTabTitle.value = 'Updated Title'
}
</script>Watched Properties
The following reactive properties are automatically monitored:
| Property | Description | Example |
|----------|-------------|---------|
| routeTabTitle | Tab title text | ref('Dashboard') |
| routeTabIcon | Tab icon class | ref('mdi-home') |
| routeTabClosable | Can tab be closed | ref(true) |
| routeTabMeta | Additional metadata | ref({ badge: 5 }) |
Dynamic Titles with Computed
Create titles that update based on your component state:
<script setup>
import { ref, computed } from 'vue'
const isLoading = ref(false)
const notifications = ref(0)
// Tab title automatically updates when dependencies change
const routeTabTitle = computed(() => {
if (isLoading.value) return 'Loading...'
if (notifications.value > 0) return `Messages (${notifications.value})`
return 'Dashboard'
})
// Tab icon changes based on state
const routeTabIcon = computed(() =>
isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-view-dashboard'
)
// Prevent closing during operations
const routeTabClosable = computed(() => !isLoading.value)
</script>Real-World Examples
Example 1: User Profile with Name
<script setup>
import { ref, computed, onMounted } from 'vue'
const user = ref(null)
const isLoading = ref(true)
const routeTabTitle = computed(() =>
isLoading.value ? 'Loading...' : `Profile - ${user.value?.name || 'Unknown'}`
)
const routeTabIcon = computed(() =>
isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-account'
)
onMounted(async () => {
user.value = await fetchUser()
isLoading.value = false
})
</script>Example 2: Form with Unsaved Changes
<script setup>
import { ref, computed } from 'vue'
const formData = ref({})
const hasUnsavedChanges = ref(false)
const routeTabTitle = computed(() =>
hasUnsavedChanges.value ? '• Edit Form' : 'Edit Form'
)
const routeTabClosable = computed(() => !hasUnsavedChanges.value)
function onChange() {
hasUnsavedChanges.value = true
}
</script>Example 3: Real-time Notifications
<script setup>
import { ref, computed, onMounted } from 'vue'
const unreadCount = ref(0)
const routeTabTitle = computed(() => {
if (unreadCount.value === 0) return 'Messages'
return `Messages (${unreadCount.value})`
})
const routeTabIcon = computed(() =>
unreadCount.value > 0 ? 'mdi-bell-badge' : 'mdi-bell-outline'
)
// Simulate real-time updates
onMounted(() => {
setInterval(() => {
unreadCount.value = Math.floor(Math.random() * 10)
}, 5000)
})
</script>Using the Composable API
For advanced use cases, use the useReactiveTab composable:
<script setup>
import { useReactiveTab } from 'vue3-router-tab'
import { ref } from 'vue'
const user = ref({ name: 'John Doe', status: 'online' })
const {
routeTabTitle,
routeTabIcon,
routeTabClosable
} = useReactiveTab({
title: () => `${user.value.name} - ${user.value.status}`,
icon: () => user.value.status === 'online' ? 'mdi-account' : 'mdi-account-off',
closable: () => user.value.status !== 'editing'
})
</script>Important Notes
- Automatic Exposure in
<script setup>: Properties defined in<script setup>are automatically exposed - no need fordefineExpose() - Reactive types: Use
ref()orcomputed()- plain values won't trigger updates - Automatic watching: No manual watchers needed - the plugin handles everything
- Performance: Only active tab components are watched to minimize overhead
Works inside KeepAlive and wrappers
<router-tab> forwards the tab bindings even when your pages are wrapped for caching. Just expose reactive values (refs/computed) and they will still be watched:
<script setup>
import { computed, ref } from 'vue'
const count = ref(3)
const isLoading = ref(false)
const routeTabTitle = computed(() => isLoading.value ? 'Loading orders…' : `Orders (${count.value})`)
const routeTabIcon = computed(() => isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-cart')
const routeTabClosable = computed(() => !isLoading.value)
</script>💡 Try it yourself: Check out the live demo at
/title-testin the example app to see all these features in action!
Options API Support
If you're using the Options API, you need to expose the properties:
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const routeTabTitle = ref('My Page')
const routeTabIcon = computed(() => 'mdi-page')
return {
routeTabTitle,
routeTabIcon
}
}
}
</script>Or with defineExpose:
<script setup>
import { ref } from 'vue'
const routeTabTitle = ref('My Page')
// Only needed if you're NOT using top-level refs in <script setup>
defineExpose({ routeTabTitle })
</script>Note: With <script setup>, top-level bindings are automatically exposed, so defineExpose is typically not needed.
Tab Closing Behavior
When a tab is closed, the plugin automatically navigates to the next available tab with the following priority:
- Next tab - The tab immediately to the right
- Previous tab - The tab immediately to the left
- First tab - If no adjacent tabs exist
- Default route - If no other tabs exist
Cookie persistence
<router-tab cookie-key="…" /> is usually all you need. If you prefer fine grained control (custom expiry, same-site, etc.) you can still use the headless helper:
<router-tab>
<template #start>
<router-tabs
cookie-key="app-tabs"
:expires-in-days="14"
fallback-route="/dashboard"
/>
</template>
</router-tab>Want to wire it up yourself?
<script setup lang="ts">
import { useRouterTabsPersistence } from 'vue3-router-tab'
useRouterTabsPersistence({
cookieKey: 'app-tabs',
expiresInDays: 30,
fallbackRoute: '/dashboard'
})
</script>The composable also exposes serialize / deserialize options so you can encrypt or customise the cookie payload.
Theme system
The plugin initialises a lightweight theme layer on install:
- Reads
tab-theme-style('light','dark', or'system'; defaults to'system'). - Reads
tab-theme-primary-color(defaults to#0f172a). - Applies the choice via
data-themeand--theme-primaryCSS variables, keeping “system” in sync with OS changes.
Override the theme at runtime:
import { setRouterTabsTheme, setRouterTabsPrimary } from 'vue3-router-tab'
setRouterTabsTheme('dark')
setRouterTabsPrimary('#22c55e')Customise the defaults with:
import { initRouterTabsTheme } from 'vue3-router-tab'
initRouterTabsTheme({
defaultStyle: 'dark',
defaultPrimary: '#0ea5e9'
})Custom rendering
You can override the default routed view by providing a #default slot. The slot receives the same values you would normally get from <RouterView v-slot>:
<router-tab cookie-key="app-tabs">
<template #default="{ Component, route }">
<Suspense>
<component :is="Component" :key="route.fullPath" />
</Suspense>
</template>
</router-tab>Advanced Features
Tab Sorting
Enable drag-and-drop tab reordering with the sortable prop:
<router-tab
:sortable="true"
@tab-sort="onTabSort"
@tab-sorted="onTabSorted"
/>Persistence Options
Fine-grained control over tab persistence:
<router-tab
:persistence="{
cookieKey: 'my-app-tabs',
expiresInDays: 30,
fallbackRoute: '/dashboard',
serialize: (snapshot) => btoa(JSON.stringify(snapshot)),
deserialize: (data) => JSON.parse(atob(data))
}"
/>Customising the Context Menu
<router-tab
:contextMenu="[
'refresh',
'close',
{ id: 'closeOthers', label: 'Close All Others' },
{
id: 'openWindow',
label: 'Open in new window',
handler: ({ target }) => window.open(target.to, '_blank')
}
]"
/>Pass false to disable the context menu entirely.
Built-in Menu Items
refresh- Refresh current tabrefreshAll- Refresh all tabsclose- Close current tabcloseLefts- Close tabs to the leftcloseRights- Close tabs to the rightcloseOthers- Close all other tabs
Programmatic API
Access the router tabs controller to programmatically manage tabs.
Using the Composable
<script setup>
import { useRouterTabs } from 'vue3-router-tab'
const tabs = useRouterTabs()
// Available methods
tabs.openTab('/users') // Open a tab
tabs.closeTab(tabId) // Close a specific tab
tabs.refreshTab(tabId) // Refresh a tab
tabs.refreshAll() // Refresh all tabs
tabs.closeAll() // Close all tabs
tabs.closeOthers(tabId) // Close all except specified tab
</script>Using Global Property
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const tabs = instance?.appContext.config.globalProperties.$tabs
// Same methods available
tabs?.openTab('/dashboard')
tabs?.refreshTab('users-123')
</script>Controller Methods
| Method | Parameters | Description |
|--------|------------|-------------|
| openTab(to, active?, replace?) | Route location, activate flag, replace flag | Open or activate a tab |
| closeTab(id, options?) | Tab ID, close options | Close a specific tab |
| refreshTab(id, force?) | Tab ID, force flag | Refresh tab component |
| refreshAll(force?) | Force flag | Refresh all tabs |
| closeAll(options?) | Close options | Close all closable tabs |
| closeOthers(id, options?) | Tab ID, options | Close all tabs except specified |
| removeTab(id, options?) | Tab ID, options | Remove tab without navigation |
Example: Custom Tab Controls
<template>
<div>
<button @click="openDashboard">Open Dashboard</button>
<button @click="refreshCurrent">Refresh Current</button>
<button @click="closeAllTabs">Close All</button>
</div>
</template>
<script setup>
import { useRouterTabs } from 'vue3-router-tab'
import { useRoute } from 'vue-router'
const tabs = useRouterTabs()
const route = useRoute()
function openDashboard() {
tabs.openTab('/dashboard', true)
}
function refreshCurrent() {
const currentTab = tabs.tabs.find(t => t.to.path === route.path)
if (currentTab) {
tabs.refreshTab(currentTab.id, true)
}
}
function closeAllTabs() {
tabs.closeAll({ force: true })
}
</script>Tab State Access
<script setup>
import { useRouterTabs } from 'vue3-router-tab'
const controller = useRouterTabs()
</script>Events
RouterTab emits events for tab interactions.
Available Events
| Event | Payload | Description |
|-------|---------|-------------|
| tab-sort | { tab, index } | Fired when tab drag starts |
| tab-sorted | { tab, fromIndex, toIndex } | Fired when tab is dropped in new position |
Usage Example
<template>
<router-tab
@tab-sort="onTabSort"
@tab-sorted="onTabSorted"
/>
</template>
<script setup>
function onTabSort({ tab, index }) {
console.log('Dragging tab:', tab.title, 'from index:', index)
}
function onTabSorted({ tab, fromIndex, toIndex }) {
console.log('Tab moved:', tab.title)
console.log('From:', fromIndex, 'To:', toIndex)
// Save new order to backend
saveTabOrder(tab, toIndex)
}
</script>Slots
start/end– positioned on either side of the tab list (ideal for toolbars or the<router-tabs>helper).default– routed content (rendered automatically by<router-tab>).
Styling
The package ships with its own CSS bundle (imported automatically). Override CSS custom properties or the router-tab__* classes to customize the appearance.
CSS Custom Properties
:root {
/* Layout */
--router-tab-header-height: 48px;
--router-tab-sticky-top: 0px;
--router-tab-header-z-index: 10;
--router-tab-padding: 16px;
--router-tab-font-size: 14px;
/* Colors */
--router-tab-primary: #0f172a;
--router-tab-background: #ffffff;
--router-tab-text: #334155;
--router-tab-border: #e2e8f0;
--router-tab-active-background: #0f172a;
--router-tab-active-text: #ffffff;
/* Icons & Buttons */
--router-tab-icon-color: #64748b;
--router-tab-button-background: #f1f5f9;
--router-tab-button-color: #0f172a;
}When router-tab is rendered below a fixed app header, set --router-tab-sticky-top so the sticky bar locks beneath that header instead of sliding under it:
<router-tab class="app-router-tab" />.app-router-tab {
--router-tab-sticky-top: var(--v-layout-top, 0px);
}Custom Styles Example
/* Change tab height */
.router-tab__header {
height: 56px;
}
/* Custom tab hover effect */
.router-tab__item:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* Custom active tab */
.router-tab__item.is-active {
background: #4f46e5;
border-radius: 8px 8px 0 0;
}
/* Custom close button */
.router-tab__item-close {
border-radius: 4px;
}
/* Dark theme adjustments */
[data-theme="dark"] .router-tab {
--router-tab-background: #1e293b;
--router-tab-text: #f1f5f9;
--router-tab-border: #334155;
}Types
import type { TabRecord, RouterTabsSnapshot, RouterTabsPersistenceOptions } from 'vue3-router-tab'Examples
Check out the example-app directory for comprehensive demos including:
- Basic Usage - Simple tab navigation
- Dynamic Titles - Reactive tab title updates (/title-test)
- Transitions - All 7 transition effects (/transition-demo)
- Composables - Using helper composables (/composable-demo)
- Advanced Features - Sorting, context menus, persistence
Run Examples
cd example-app
npm install
npm run devTroubleshooting
Tab titles not updating
Problem: Tab title doesn't change when component state updates.
Solution: Ensure you're using reactive refs or computed properties:
<!-- ✅ Correct -->
<script setup>
const routeTabTitle = ref('My Page') // Reactive
</script>
<!-- ❌ Wrong -->
<script setup>
const routeTabTitle = 'My Page' // Plain string - won't update
</script>Transitions not working
Problem: Page transitions don't show when refreshing tabs.
Solution: Make sure you're not using a custom default slot that overrides transitions. Remove the custom slot or include transition components:
<!-- ✅ Default behavior with transitions -->
<router-tab page-transition="router-tab-fade" />
<!-- ❌ Custom slot without transitions -->
<router-tab>
<template #default="{ Component }">
<component :is="Component" /> <!-- No transition! -->
</template>
</router-tab>
<!-- ✅ Custom slot with transitions -->
<router-tab>
<template #default="{ Component }">
<transition name="router-tab-fade" mode="out-in">
<component :is="Component" />
</transition>
</template>
</router-tab>Tabs not persisting on refresh
Problem: Tabs are lost when refreshing the browser.
Solution: Add the cookie-key prop:
<router-tab cookie-key="my-app-tabs" />Check that cookies are enabled in the browser and not being blocked.
KeepAlive not working
Problem: Component state is lost when switching tabs.
Solution: Ensure :keep-alive="true" (default) and components are properly keyed:
// In router config
meta: {
keepAlive: true, // Enable for this route
key: 'fullPath' // Unique key per instance
}TypeScript errors
Problem: TypeScript shows errors for router tab properties.
Solution: Import types and use them:
import type { TabRecord, RouterTabsOptions } from 'vue3-router-tab'
const options: RouterTabsOptions = {
keepAlive: true,
maxAlive: 10
}Browser Support
- Chrome/Edge ≥ 90
- Firefox ≥ 88
- Safari ≥ 14
- Modern mobile browsers
Contributing
Contributions are welcome! Please read the contributing guidelines first.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT
Made with ❤️ by the Vue community
