Examples

Tabs

Dynamic tab management with shared context and active state.

A dynamic tab system demonstrating context sharing and active state management.

3 tab(s), Active: tab-1

Project Structure

tabs/
├── index.ts           # Shared types and injection key
├── Tabs.vue           # Parent component managing tabs
└── TabItem.vue        # Individual tab component

Shared Types (index.ts)

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';

Parent Component (Tabs.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>

Child Component (TabItem.vue)

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>

Key Concepts

Context Sharing

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.

Context Sharing

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 ?? '',
  };
}

Dynamic Tab Management

Tabs can be dynamically added and removed. When tabsData changes, child components with watchData: true automatically update their data from the shared context.

How It Works

  1. Parent creates desk with shared context (activeTab, selectTab, closeTab, tabsData)
  2. Child tabs check in automatically and retrieve their data from the shared context
  3. Data synchronizes when tabsData changes (via watchData: true)
  4. Active state is shared across all components via the context
  5. Dynamic add/remove works automatically with check-in/check-out

Usage

This pattern is ideal for:

  • Tab interfaces with dynamic content
  • Accordion components
  • Multi-step forms
  • Navigation menus
  • Any UI requiring shared active state