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.
Northstar workspace
Northstar workspace
No action yet
npm install @getdom/studio<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
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
items | array | Array<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. |
title | string | string | 'Menu' | Root page heading. |
height | string | string | 'min(24rem, calc(100dvh - 6rem))' | Bounded container height. Use 100% in an explicitly sized parent. |
path | array | Array<unknown> | — | Controlled path of page ids starting with root. Bind with v-model:path. |
animate | boolean | boolean | true | Enable sliding transitions; reduced-motion preferences are respected. |
Auto-generated from Menu stack.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #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
| Name | Payload | Description |
|---|---|---|
| @select | (item) | A leaf was selected. Close the containing popover here if desired. |
| @update:path | ( | 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.