A dynamic tab system demonstrating context sharing and active state management.
tabs/
├── index.ts # Shared types and injection key
├── Tabs.vue # Parent component managing tabs
└── TabItem.vue # Individual tab component
import type { InjectionKey, ComputedRef, Ref } from 'vue';
import type { DeskCore } from '#vue-airport/composables/useCheckIn';
export interface TabItemData {
label: string;
content: string;
icon?: string;
}
export interface TabItemContext {
activeTab: Ref<string | number>;
selectTab: (id: string | number) => void;
closeTab: (id: string | number) => void;
tabsCount: ComputedRef<number>;
tabsData: Ref<Array<TabItemData & { id: string | number }>>;
}
export const TABS_DESK_KEY: InjectionKey<DeskCore<TabItemData> & TabItemContext> =
Symbol('tabsDesk');
export { default as Tabs } from './Tabs.vue';
export { default as TabItem } from './TabItem.vue';
Creates the desk with shared context for active tab management:
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useCheckIn } from '#vue-airport/composables/useCheckIn';
import { type TabItemData, type TabItemContext, TABS_DESK_KEY, TabItem } from '.';
/**
* Tabs Example - Dynamic Tab Management
*
* Demonstrates:
* - Creating a desk with shared context
* - Dynamic tab creation and deletion
* - Auto check-in of child components
* - Context sharing between components
*/
// Reactive reference to store the active tab ID
const activeTabId = ref<string | number>('tab-1');
// State to manage all tabs
const tabsData = ref<
Array<{
id: string;
label: string;
content: string;
icon?: string;
}>
>([
{
id: 'tab-1',
label: 'Home',
content: 'Welcome to the tabs demo!',
icon: 'i-heroicons-home',
},
{
id: 'tab-2',
label: 'Settings',
content: 'Application configuration',
icon: 'i-heroicons-cog-6-tooth',
},
{
id: 'tab-3',
label: 'Profile',
content: 'User information',
icon: 'i-heroicons-user',
},
]);
// Function to change the active tab
const selectTab = (id: string | number) => {
activeTabId.value = id;
};
// Function to dynamically add a new tab
const addTab = () => {
const id = `tab-${Date.now()}`;
tabsData.value.push({
id,
label: `Tab ${tabsData.value.length + 1}`,
content: `Content of tab ${tabsData.value.length + 1}`,
icon: 'i-heroicons-document-text',
});
selectTab(id);
};
// Function to close a tab
const closeTab = (id: string | number) => {
// Keep at least one tab open
if (tabsData.value.length <= 1) return;
const index = tabsData.value.findIndex((t) => t.id === id);
if (index !== -1) {
tabsData.value.splice(index, 1);
}
// If the active tab is closed, select the first available tab
if (activeTabId.value === id && tabsData.value.length > 0) {
const firstTab = tabsData.value[0];
if (firstTab) {
activeTabId.value = firstTab.id;
}
}
};
// Create a desk with context to share the active tab state and helpers
const { createDesk } = useCheckIn<TabItemData, TabItemContext>();
createDesk(TABS_DESK_KEY, {
devTools: true,
debug: false,
context: {
activeTab: activeTabId,
selectTab,
closeTab,
tabsCount: computed(() => tabsData.value.length),
tabsData,
},
});
// Computed property for the active tab's content
const activeTabContent = computed(() => {
const tab = tabsData.value.find((t) => t.id === activeTabId.value);
return tab?.content || '';
});
</script>
<template>
<div>
<div class="flex gap-4 items-center mb-4 border-b border-gray-200 dark:border-gray-800 pb-2">
<div class="flex gap-1 flex-1 overflow-x-auto">
<TabItem
v-for="tab in tabsData"
:id="tab.id"
:key="tab.id"
/>
</div>
<UButton size="sm" icon="i-heroicons-plus" @click="addTab"> New Tab </UButton>
</div>
<div
class="relative overflow-hidden p-6 min-h-[150px] bg-white dark:bg-gray-800 rounded-md mb-4"
>
<Transition name="slide-fade" mode="out-in">
<p :key="activeTabId">{{ activeTabContent }}</p>
</Transition>
</div>
<div
class="p-3 bg-gray-50 dark:bg-gray-900 rounded-md text-sm text-gray-600 dark:text-gray-400"
>
<strong>Debug:</strong> {{ tabsData.length }} tab(s), Active: {{ activeTabId }}
</div>
</div>
</template>
Automatically checks in and retrieves tab data from the desk context:
<script setup lang="ts">
import { computed } from 'vue';
import { useCheckIn } from '#vue-airport/composables/useCheckIn';
import { type TabItemData, type TabItemContext, TABS_DESK_KEY } from '.';
/**
* Tab Item Component
*
* Individual tab component that automatically checks in to the desk.
*/
const props = defineProps<{
id?: string | number;
}>();
const emit = defineEmits<{
select: [id: string | number];
close: [id: string | number];
}>();
// Check in to the tabs desk and capture the desk (which contains provided context)
const { checkIn } = useCheckIn<TabItemData, TabItemContext>();
const { desk } = checkIn(TABS_DESK_KEY, {
id: props.id,
autoCheckIn: true,
watchData: true,
debug: false,
data: (desk) => {
const tabData = desk.tabsData.value.find(
(t: TabItemData & { id: string | number }) => t.id === props.id
);
return {
label: tabData?.label ?? '',
content: tabData?.content ?? '',
icon: tabData?.icon ?? undefined,
};
},
});
const isActive = computed(() => {
if (!desk || !desk.activeTab) return false;
try {
return desk.activeTab.value === props.id;
} catch {
return false;
}
});
const canClose = computed(() => {
if (!desk || !desk.tabsCount) return true;
try {
return desk.tabsCount.value > 1;
} catch {
return true;
}
});
// Retrieve data from the desk context
const tabData = computed(() => {
return desk?.tabsData?.value.find((t) => t.id === props.id);
});
const onSelect = () => {
if (desk && typeof desk.selectTab === 'function') {
desk.selectTab(props.id as any);
} else {
emit('select', props.id as any);
}
};
const onClose = () => {
if (desk && typeof desk.closeTab === 'function') {
desk.closeTab(props.id as any);
} else {
emit('close', props.id as any);
}
};
</script>
<template>
<div class="relative flex items-center gap-1 h-12">
<UButton
:leading-icon="tabData?.icon"
color="neutral"
variant="ghost"
class="rounded-t-md rounded-b-none whitespace-nowrap"
@click="onSelect"
>
{{ tabData?.label }}
</UButton>
<UButton
v-if="canClose"
size="xs"
color="neutral"
variant="ghost"
icon="i-heroicons-x-mark"
@click="onClose"
/>
<div v-if="isActive" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary z-10" />
</div>
</template>
The desk is created with a context object that contains the activeTab reference, methods to selectTab and closeTab, and the tabsData reference:
const { createDesk } = useCheckIn<TabItemData, TabItemContext>();
createDesk(TABS_DESK_KEY, {
devTools: true,
debug: false,
context: {
activeTab: activeTabId,
selectTab,
closeTab,
tabsCount: computed(() => tabsData.value.length),
tabsData, // Shared reference to all tabs data
},
});
This allows all child components to access the shared active state and retrieve their own data from the context.
The desk provides context to all child components. The data function receives the desk as a parameter:
data: (desk) => {
const tabData = desk.tabsData.value.find(t => t.id === props.id);
return {
label: tabData?.label ?? '',
content: tabData?.content ?? '',
};
}
Tabs can be dynamically added and removed. When tabsData changes, child components with watchData: true automatically update their data from the shared context.
watchData: true)This pattern is ideal for: