Component

Vue Menu Stack Component

<DomMenuStack>

Nested navigation that slides inside its container on mobile and desktop. Reuses App stack transitions, retained page state, scrolling, and focus. Each item has a unique id and label, plus children, a component and props, or a named slot for richer pages. Leaf items accept to or href, or emit select for application actions.

Workspace navigation

Pages and menus in one container

Explore five levels, edit your profile, or open the same menu in a popover. Desktop and mobile use the same contained slide navigation.

Open your account, edit the name, and go Back. The page keeps your changes while this menu is mounted.

No action yet

Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { markRaw, ref } from 'vue';
import { DomAppProfile, DomMenuStack, DomPopover } from '@getdom/studio';

const selected = ref('No action yet');
const path = ref(['root']);
const items = [
	{
		id: 'account', label: 'Your account', description: 'Profile, preferences, and security',
		component: markRaw(DomAppProfile),
		props: { name: 'Avery Stone', email: 'avery@northstar.app', src: '/favicon.svg', actionLabel: '' },
		children: [
			{ id: 'profile', label: 'Edit profile', slot: 'profile' },
			{ id: 'preferences', label: 'Preferences', children: [
				{ id: 'notifications', label: 'Notifications', children: [
					{ id: 'delivery', label: 'Delivery channels', children: [
						{ id: 'email', label: 'Email digests' },
						{ id: 'push', label: 'Push notifications' },
					] },
				] },
			] },
		],
	},
	{ id: 'workspace', label: 'Workspace', children: [
		{ id: 'members', label: 'Team members', slot: 'members', children: [{ id: 'invite', label: 'Invite a teammate' }] },
		{ id: 'billing', label: 'Billing', description: 'Workspace owner access required', disabled: true },
	] },
	{ id: 'docs', label: 'Documentation', to: '/components' },
];
</script>

<template>
	<div class="w-full max-w-lg space-y-4">
		<p class="text-sm text-muted-fg">Open your account, edit the name, and go Back. The page keeps your changes while this menu is mounted.</p>
		<DomMenuStack v-model:path="path" :items="items" title="Northstar workspace" height="26rem" @select="selected = $event.label">
			<template #profile>
				<label class="block space-y-2 p-4 text-sm font-medium">
					<span>Display name</span>
					<input value="Avery Stone" class="w-full rounded-lg border border-border bg-canvas p-3" />
				</label>
			</template>
			<template #members>
				<div class="space-y-2 p-4">
					<h2 class="font-semibold">Your team</h2>
					<p class="text-sm text-muted-fg">Avery Stone · Owner<br />Jules Park · Designer</p>
				</div>
			</template>
		</DomMenuStack>
		<div class="flex flex-wrap items-center gap-3">
			<DomPopover label="Open workspace menu" :arrow="false" position="bottom-start" padding="p-0" width="w-[min(22rem,calc(100vw-2rem))]">
				<template #default="{ close }">
					<DomMenuStack :items="items" title="Northstar workspace" @select="selected = $event.label; close()">
						<template #profile>
							<label class="block space-y-2 p-4 text-sm"><span>Display name</span><input value="Avery Stone" class="w-full rounded-lg border border-border bg-canvas p-3" /></label>
						</template>
						<template #members><p class="p-4 text-sm text-muted-fg">Avery Stone and Jules Park</p></template>
					</DomMenuStack>
				</template>
			</DomPopover>
			<p aria-live="polite" class="text-sm text-muted-fg">{{ selected }}</p>
		</div>
	</div>
</template>

Composition

Build your menu tree

Give every item a unique string id and a label. The id root is reserved. Add children for another menu, component and props for a Vue page, or slot for named slot content. Page content appears above its child rows.

Components and slots receive screen and stack. Call stack.push(id) or stack.back() from custom controls. Keep Vue component objects outside deep reactive data or use markRaw.

Leaf rows use to for Vue Router, href for native links, or emit select for actions. A branch always opens its page; add a child overview link if it also has a route. The popover owns dismissal, so close it on select when desired.

Set a bounded height, or use 100% inside a sized parent. Each page owns scrolling and keeps its content while mounted. Bind v-model:path to control navigation, beginning with ['root']. Stable ids preserve screen identity.

Props

Control props

NameTypeTSDefaultDescription
itemsarrayArray<unknown>[]Nested items: unique id, label, description, meta, disabled, children, component, props, slot, to, or href. Page items open their children; use a leaf for a route or action.
titlestringstring'Menu'Root page heading.
heightstringstring'min(24rem, calc(100dvh - 6rem))'Bounded container height. Use 100% in an explicitly sized parent.
patharrayArray<unknown>Controlled path of page ids starting with root. Bind with v-model:path.
animatebooleanbooleantrueEnable sliding transitions; reduced-motion preferences are respected.

Auto-generated from Menu stack.props and inline _edit hints.

Slots

NameScopeDescription
#root{ screen, stack }Content above the root menu rows.
#(item.slot){ screen, stack }Page content above child rows. Set an item’s slot to this slot name; components receive screen and stack props too.

Events

NamePayloadDescription
@select(item)A leaf was selected. Close the containing popover here if desired.
@update:path(path
path: string[];
: string[])
Two-way stack path, starting with root.
@navigate({ path, previousPath, direction, screen, reason })A contained page opened or returned.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.