Component

Tabs

<dom-tabs>

Roving-tabindex tab list with managed ARIA, keyboard navigation, and panel linking.

Playground

Try every prop live

Tabs playground

Edit props in the inspector — switch tabs to preview panel slots and the active tab key.

Overview panel content.

Pricing panel content.

Changelog panel content.

Playground.vuevue
vue
<script setup>
import { reactive } from 'vue';
import { DomTabs } from '@getdom/studio/vue';

const data = reactive({
	  "modelValue": "overview",
	  "tabs": [
	    {
	      "key": "overview",
	      "label": "Overview"
	    },
	    {
	      "key": "pricing",
	      "label": "Pricing"
	    },
	    {
	      "key": "changelog",
	      "label": "Changelog"
	    }
	  ],
	  "variant": "pill",
	  "fill": false
	});
</script>

<template>
	<DomTabs
		v-bind="data"
		@update:modelValue="data.modelValue = $event"
	/>
</template>

Demo

Live preview

Overview content. Tabs preserve focus and announce panel changes to assistive tech.
Pricing content. Use the arrow keys to move between tabs.
Changelog content. Each panel is keyboard-reachable.

Variant

Page tabs

Page tabs

Editor-style tabs can use variant="page", scoped tab labels, and reactive tab arrays.

src/app/dashboard/page.tsx

Page tabs sit flush with the panel, so they work well for editor files, browser-like workspaces, and record pages.

src/components/MissionControl.tsx

Use the tab slot to add status dots, icons, or compact metadata while keeping the headless tab behavior intact.

src/lib/agent-client.ts

The same component can switch between pill navigation and page-style workspace navigation with the variant prop.

vue
<script setup>
import { ref } from 'vue';
import { DomButton, DomTabs } from '../../../lib/vue';

const active = ref('dashboard-page');
const openTabs = ref([
	{ key: 'dashboard-page', label: 'page.tsx', path: 'src/app/dashboard/page.tsx', status: 'modified' },
	{ key: 'mission-control', label: 'MissionControl.tsx', path: 'src/components/MissionControl.tsx', status: 'added' },
	{ key: 'agent-client', label: 'agent-client.ts', path: 'src/lib/agent-client.ts', status: '' },
]);
const availableTabs = [
	{ key: 'dashboard-page', label: 'page.tsx', path: 'src/app/dashboard/page.tsx', status: 'modified' },
	{ key: 'mission-control', label: 'MissionControl.tsx', path: 'src/components/MissionControl.tsx', status: 'added' },
	{ key: 'agent-client', label: 'agent-client.ts', path: 'src/lib/agent-client.ts', status: '' },
	{ key: 'task-card', label: 'TaskCard.tsx', path: 'src/components/TaskCard.tsx', status: 'modified' },
];

function openTab(tab) {
	if (!openTabs.value.some((item) => item.key === tab.key)) openTabs.value.push(tab);
	active.value = tab.key;
}

function statusClass(status) {
	if (status === 'added') return 'bg-success';
	if (status === 'modified') return 'bg-warning';
	return 'bg-muted-fg/40';
}
</script>

<template>
	<div class="w-full max-w-3xl overflow-hidden rounded-lg border border-border bg-canvas shadow-sm">
		<DomTabs v-model="active" :tabs="openTabs" variant="page">
			<template #tab="{ tab }">
				<span class="size-2 shrink-0 rounded-full" :class="statusClass(tab.status)"></span>
				<span class="min-w-0 truncate">{{ tab.label }}</span>
			</template>
			<template #dashboard-page>
				<div class="p-5 text-sm text-muted-fg">
					<p class="font-mono text-xs text-primary">src/app/dashboard/page.tsx</p>
					<p class="mt-3 leading-6">Page tabs sit flush with the panel, so they work well for editor files, browser-like workspaces, and record pages.</p>
				</div>
			</template>
			<template #mission-control>
				<div class="p-5 text-sm text-muted-fg">
					<p class="font-mono text-xs text-primary">src/components/MissionControl.tsx</p>
					<p class="mt-3 leading-6">Use the tab slot to add status dots, icons, or compact metadata while keeping the headless tab behavior intact.</p>
				</div>
			</template>
			<template #agent-client>
				<div class="p-5 text-sm text-muted-fg">
					<p class="font-mono text-xs text-primary">src/lib/agent-client.ts</p>
					<p class="mt-3 leading-6">The same component can switch between pill navigation and page-style workspace navigation with the variant prop.</p>
				</div>
			</template>
			<template #task-card>
				<div class="p-5 text-sm text-muted-fg">
					<p class="font-mono text-xs text-primary">src/components/TaskCard.tsx</p>
					<p class="mt-3 leading-6">Tabs can be added from reactive arrays as users open new pages or files.</p>
				</div>
			</template>
		</DomTabs>
		<div class="flex flex-wrap gap-2 border-t border-border bg-secondary/40 p-3">
			<DomButton
				v-for="tab in availableTabs"
				:key="tab.key"
				size="sm"
				variant="secondary"
				@click="openTab(tab)"
			>
				Open {{ tab.label }}
			</DomButton>
		</div>
	</div>
</template>

Usage

Vue

vue
<script setup>
import { DomTabs } from '@getdom/studio/vue';
import { ref } from 'vue';
const active = ref('overview');
const tabs = [
	{ key: 'overview', label: 'Overview' },
	{ key: 'pricing', label: 'Pricing' },
];
</script>

<template>
	<DomTabs v-model="active" :tabs="tabs">
		<template #overview>Overview content…</template>
		<template #pricing>Pricing content…</template>
	</DomTabs>
</template>

Usage

Web component

html
<dom-tabs value="overview">
	<div role="tablist">
		<button data-tab="overview">Overview</button>
		<button data-tab="pricing">Pricing</button>
	</div>
	<div data-panel="overview">Overview…</div>
	<div data-panel="pricing">Pricing…</div>
</dom-tabs>

Props

Props

NameTypeDefaultDescription
v-modelstringfirst tab keyActive tab key.
tabsArray<{ key, label }>Tabs to render.
variant'pill' | 'page''pill'Use page for editor-style tabs attached to a panel.
fillbooleanfalseStretch the tab list and active panel inside a flex parent.

Keyboard

  • ← / →Move to previous / next tab.
  • Home / EndJump to first / last tab.
  • Enter / SpaceActivate focused tab.