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.
npm install 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.
- π― 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
``bash`
npm install vue3-router-tabor
pnpm add vue3-router-tabor
yarn add vue3-router-tab
`ts
// 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')
`
`vue`
That's it! You now have a fully functional tabbed router interface.
`vue`
:sortable="true"
:keep-alive="true"
/>
`vue`
:keep-alive="true"
:max-alive="10"
:keep-last-tab="true"
:sortable="true"
page-transition="router-tab-fade"
tab-transition="router-tab-zoom"
/>
Configure your routes with tab metadata:
`ts`
// router/index.ts
const routes = [
{
path: '/',
component: Home,
meta: {
title: 'Home',
icon: 'mdi-home',
closable: true,
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,
},
},
]
Make your tabs dynamic by exposing reactive properties in your components:
`vue
{{ pageTitle }}
Loading...
`
Choose from 7 built-in transition effects:
`vue
`
| 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 |
`ts
import { setRouterTabsTheme } from 'vue3-router-tab'
// Switch themes at runtime
setRouterTabsTheme('dark')
setRouterTabsTheme('light')
setRouterTabsTheme('system') // Follows OS preference
`
`ts
import { setRouterTabsPrimary } from 'vue3-router-tab'
setRouterTabsPrimary({
primary: '#3b82f6',
background: '#ffffff',
text: '#1f2937',
activeBackground: '#3b82f6',
activeText: '#ffffff',
border: '#e5e7eb'
})
`
`css
: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;
}
`
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
`vue
`
- Arrow Keys: Navigate between tabs
- Enter/Space: Activate selected tab
- Delete/Backspace: Close current tab (if closable)
- Home/End: Jump to first/last tab
| 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 |
| Event | Payload | Description |
|-------|---------|-------------|
| tab-sort | { tab: TabRecord, index: number } | Tab drag started |tab-sorted
| | { tab: TabRecord, fromIndex: number, toIndex: number } | Tab reordered |
| Slot | Props | Description |
|------|-------|-------------|
| start | - | Content before tab list |end
| | - | Content after tab list |default
| | { Component, route } | Custom page rendering |
`vue`
`ts
interface RouterTabsContext {
// Navigation
openTab(to: RouteLocationRaw, replace?: boolean, refresh?: boolean | 'sameTab'): Promise
closeTab(id?: string, options?: CloseTabOptions): Promise
// Management
refreshTab(id?: string, force?: boolean): Promise
refreshAll(force?: boolean): Promise
removeTab(id: string, opts?: RemoveTabOptions): Promise
// Cache Control
setTabAlive(id: string, alive: boolean): void
evictCache(id: string): void
clearCache(): void
getCacheKeys(): string[]
// State
reset(route?: RouteLocationRaw): Promise
reload(): Promise
// Utilities
getRouteKey(route: RouteLocationNormalizedLoaded | RouteLocationRaw): string
matchRoute(route: RouteLocationNormalizedLoaded | RouteLocationRaw): RouteMatchResult
// Persistence
snapshot(): RouterTabsSnapshot
hydrate(snapshot: RouterTabsSnapshot): Promise
}
`
`vue`
'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')
}
]"
/>
`vue`
`vue`
cookieKey: 'my-app-tabs',
expiresInDays: 30,
path: '/',
secure: true,
sameSite: 'strict',
serialize: (snapshot) => encrypt(JSON.stringify(snapshot)),
deserialize: (data) => JSON.parse(decrypt(data))
}"
/>
`vue`
`vue`
`vue`
` vue
:sortable="true"
page-transition="router-tab-fade"
@tab-sorted="onTabSorted"
/>
`
`vue`
Tabs not updating titles
- Ensure you're using ref() or computed() for reactive properties
- Check that properties are properly exposed in
`
Create your own transitions by defining CSS classes:
`css
/ 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);
}
`
`vue`
- 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
Vue3 Router Tab automatically watches for reactive properties in your page components and updates the corresponding tab information in real-time.
Simply expose reactive properties from your component:
`vue
{{ routeTabTitle }}
`
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 }) |
Create titles that update based on your component state:
`vue`
#### Example 1: User Profile with Name
`vue`
#### Example 2: Form with Unsaved Changes
`vue`
#### Example 3: Real-time Notifications
`vue`
For advanced use cases, use the useReactiveTab composable:
`vue`
1. Automatic Exposure in
`
> π‘ Try it yourself: Check out the live demo at /title-test in the example app to see all these features in action!
If you're using the Options API, you need to expose the properties:
`vue`
Or with defineExpose:
`vue`
Note: With
`serialize
The composable also exposes / deserialize options so you can encrypt or customise the cookie payload.
The plugin initialises a lightweight theme layer on install:
- Reads tab-theme-style ('light', 'dark', or 'system'; defaults to 'system').tab-theme-primary-color
- Reads (defaults to #0f172a).data-theme
- Applies the choice via and --theme-primary CSS variables, keeping βsystemβ in sync with OS changes.
Override the theme at runtime:
`ts
import { setRouterTabsTheme, setRouterTabsPrimary } from 'vue3-router-tab'
setRouterTabsTheme('dark')
setRouterTabsPrimary('#22c55e')
`
Customise the defaults with:
`ts
import { initRouterTabsTheme } from 'vue3-router-tab'
initRouterTabsTheme({
defaultStyle: 'dark',
defaultPrimary: '#0ea5e9'
})
`
You can override the default routed view by providing a #default slot. The slot receives the same values you would normally get from :
`vue`
Enable drag-and-drop tab reordering with the sortable prop:
`vue`
@tab-sort="onTabSort"
@tab-sorted="onTabSorted"
/>
Fine-grained control over tab persistence:
`vue`
cookieKey: 'my-app-tabs',
expiresInDays: 30,
fallbackRoute: '/dashboard',
serialize: (snapshot) => btoa(JSON.stringify(snapshot)),
deserialize: (data) => JSON.parse(atob(data))
}"
/>
`vue`
'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.
- refresh - Refresh current tabrefreshAll
- - Refresh all tabs close
- - Close current tabcloseLefts
- - Close tabs to the leftcloseRights
- - Close tabs to the rightcloseOthers
- - Close all other tabs
Access the router tabs controller to programmatically manage tabs.
`vue`
`vue`
| 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 |
`vue
`
`vue`
RouterTab emits events for tab interactions.
| 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 |
`vue
@tab-sorted="onTabSorted"
/>
`
- start / end β positioned on either side of the tab list (ideal for toolbars or the helper).default
- β routed content (rendered automatically by ).
The package ships with its own CSS bundle (imported automatically). Override CSS custom properties or the router-tab__* classes to customize the appearance.
`css`
:root {
/ Layout /
--router-tab-header-height: 48px;
--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;
}
`css
/ 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;
}
`
`ts`
import type { TabRecord, RouterTabsSnapshot, RouterTabsPersistenceOptions } from 'vue3-router-tab'
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
`bash`
cd example-app
npm install
npm run dev
Problem: Tab title doesn't change when component state updates.
Solution: Ensure you're using reactive refs or computed properties:
`vue
`
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:
`vue
`
Problem: Tabs are lost when refreshing the browser.
Solution: Add the cookie-key prop:
`vue`
Check that cookies are enabled in the browser and not being blocked.
Problem: Component state is lost when switching tabs.
Solution: Ensure :keep-alive="true" (default) and components are properly keyed:
`ts`
// In router config
meta: {
keepAlive: true, // Enable for this route
key: 'fullPath' // Unique key per instance
}
Problem: TypeScript shows errors for router tab properties.
Solution: Import types and use them:
`ts
import type { TabRecord, RouterTabsOptions } from 'vue3-router-tab'
const options: RouterTabsOptions = {
keepAlive: true,
maxAlive: 10
}
`
- Chrome/Edge β₯ 90
- Firefox β₯ 88
- Safari β₯ 14
- Modern mobile browsers
Contributions are welcome! Please read the contributing guidelines first.
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature'
3. Commit your changes ()git push origin feature/amazing-feature`)
4. Push to the branch (
5. Open a Pull Request
MIT
---
Made with β€οΈ by the Vue community