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
Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { reactive } from 'vue';
import { DomTabs } from '@getdom/studio';

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.

Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomButton, DomTabs } from '@getdom/studio';

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>

Variant

Flat tabs

Flat tabs

Flat tabs use a simple active underline for page-level navigation without a contained control surface.

Recent workspace activity appears here.
Analytics and performance trends appear here.
Workspace settings appear here.
Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomTabs } from '@getdom/studio';

const active = ref('activity');
const tabs = [
	{ key: 'activity', label: 'Activity' },
	{ key: 'analytics', label: 'Analytics' },
	{ key: 'settings', label: 'Settings' },
];
</script>

<template>
	<div class="w-full max-w-2xl">
		<DomTabs v-model="active" :tabs="tabs" variant="flat">
			<template #activity>
				<div class="rounded-xl border border-border bg-canvas p-5 text-sm text-muted-fg">
					Recent workspace activity appears here.
				</div>
			</template>
			<template #analytics>
				<div class="rounded-xl border border-border bg-canvas p-5 text-sm text-muted-fg">
					Analytics and performance trends appear here.
				</div>
			</template>
			<template #settings>
				<div class="rounded-xl border border-border bg-canvas p-5 text-sm text-muted-fg">
					Workspace settings appear here.
				</div>
			</template>
		</DomTabs>
	</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' | 'flat' | 'page''pill'Use flat for underlined navigation or 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.