MagicMenu
MagicMenu is a flexible collection of components intended to build various types of menus and navigation.
<template>
<magic-menu-provider
id="magic-menu-default-demo"
class="bg-surface-high inline-flex gap-2 rounded-2xl p-1"
>
<magic-menu-view
v-for="(menuItem, index) in menu"
:id="`magic-menu-default-demo-view-${menuItem.label}`"
:key="index"
>
<magic-menu-trigger v-slot="{ viewActive }" as-child>
<m-button :mode="viewActive ? 'translucent' : 'ghost'" size="xs">
{{ menuItem.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content :middleware="offsetMiddleware">
<div class="bg-surface-high w-[220px] rounded-2xl p-1">
<nested-demo-menu
v-for="(item, itemIndex) in menuItem.items"
:key="itemIndex"
:item="item"
/>
</div>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</template>
<script lang="ts" setup>
import { offset } from '@floating-ui/dom'
import { MButton } from '@maas/mirror/vue'
import NestedDemoMenu from './components/NestedDemoMenu.vue'
import '@maas/vue-equipment/utils/css/keyframes/fade-in.css'
import '@maas/vue-equipment/utils/css/keyframes/fade-out.css'
const offsetMiddleware = [offset({ crossAxis: -4, mainAxis: 8 })]
const menu = [
{
label: 'Edit',
type: 'button',
items: [
{ label: 'Undo', cmd: '⌘ Z' },
{ label: 'Redo', cmd: '⇧ ⌘ Z' },
{
label: 'Find',
items: [
{ label: 'Search the web…' },
{ label: 'Find…' },
{ label: 'Find Next' },
{ label: 'Find Previous' },
],
},
{ label: 'Cut', cmd: '⌘ X' },
{ label: 'Copy', cmd: '⌘ C' },
{ label: 'Paste', cmd: '⌘ V' },
],
},
{
label: 'File',
type: 'ghost',
items: [
{ label: 'New Tab', cmd: '⌘ T' },
{ label: 'New Window', cmd: '⌘ W' },
{ label: 'New Incognito Window', disabled: true },
{
label: 'Share',
items: [
{ label: 'Email Link', disabled: true },
{ label: 'Messages' },
{ label: 'Notes' },
{
label: 'Socials',
items: [{ label: 'Instagram' }, { label: 'Bluesky' }],
},
],
},
{ label: 'Print', cmd: '⌘ P' },
],
},
{
label: 'View',
type: 'ghost',
items: [
{ label: 'Show Bookmarks', cmd: '⇧ ⌘ B' },
{ label: 'Show Full URLs' },
{ label: 'Reload', cmd: '⌘ R' },
{ label: 'Force Reload', cmd: '⇧ ⌘ R' },
{ label: 'Fullscreen', cmd: '⌘ F' },
{ label: 'Hide Sidebar' },
],
},
{
label: 'Profiles',
type: 'ghost',
items: [
{
label: 'Christoph Jeworutzki',
items: [
{ label: 'User Settings' },
{ label: 'Edit Profile' },
{ label: 'Subscribtions' },
],
},
{
label: 'Robin Scholz',
items: [
{ label: 'User Settings' },
{ label: 'Edit Profile' },
{ label: 'Subscribtions' },
],
},
{ label: 'Edit…' },
{ label: 'Add Profile…' },
],
},
]
</script>Overview
Anatomy
<template>
<magic-menu-provider id="your-menu-id">
<magic-menu-view>
<magic-menu-trigger>
<!-- your content -->
</magic-menu-trigger>
<magic-menu-content>
<magic-menu-item>
<!-- your content -->
</magic-menu-item>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</template>
<script setup>
const { selectView } = useMagicMenu('your-menu-id')
</script>Installation
CLI
Add @maas/vue-equipment to your dependencies.
pnpm install @maas/vue-equipmentnpm install @maas/vue-equipmentyarn add @maas/vue-equipmentbun install @maas/vue-equipmentVue
If you are using Vue, import and add MagicMenuPlugin to your app.
import { createApp } from 'vue'
import { MagicMenuPlugin } from '@maas/vue-equipment/plugins/MagicMenu'
const app = createApp({})
app.use(MagicMenuPlugin)Nuxt
The menu is available as a Nuxt module. In your Nuxt config file add @maas/vue-equipment/nuxt to your modules and add MagicMenu to the plugins in your configuration.
export default defineNuxtConfig({
modules: ['@maas/vue-equipment/nuxt'],
vueEquipment: {
plugins: ['MagicMenu'],
},
})Composable
In order to interact with the menu from anywhere within your app, we provide a useMagicMenu composable. Import it directly when needed.
import { useMagicMenu } from '@maas/vue-equipment/plugins/MagicMenu'
const { selectView } = useMagicMenu('your-menu-id')
function handleClick() {
selectView('your-view-id')
}TIP
If you have installed the component as a Nuxt module, the composable will be auto-imported and is automatically available in your Nuxt app.
Peer Dependencies
If you haven’t installed the required peer dependencies automatically, you’ll need to install the following packages manually.
Installation
pnpm install @floating-ui/vue @maas/vue-primitive @nuxt/kit @vueuse/core defunpm install @floating-ui/vue @maas/vue-primitive @nuxt/kit @vueuse/core defuyarn add @floating-ui/vue @maas/vue-primitive @nuxt/kit @vueuse/core defubun install @floating-ui/vue @maas/vue-primitive @nuxt/kit @vueuse/core defuAPI Reference
MagicMenuProvider
The MagicMenuProvider wraps the menu and configures all child components according to the provided options.
Props
| Prop | Type | Required |
|---|---|---|
MaybeRef<string> | true | |
boolean | false | |
MagicMenuOptions | false |
Options
To customize the menu, override the necessary options. Any custom options will be merged with the default options.
| Option | Type | Default |
|---|---|---|
menubar | ||
boolean | false | |
boolean | object | object | |
boolean | true | |
string | ||
string | 'magic-menu-content--fade' | |
string | 'magic-menu-channel' | |
floating.strategy | ||
delay.mouseenter | number | |
delay.mouseleave | number | |
delay.click | number | 0 |
delay.rightClick | number | 0 |
TIP
Make sure to not use translate3D or similar properties for custom transitions. This can interfere with positioning and cause the menu to be misplaced.
MagicMenuView
Groups menu items into a single navigation level and tracks its active state.
Props
| Prop | Type | Required |
|---|---|---|
MaybeRef<string> | false | |
false |
Slot Props
| Prop | Type | Description |
|---|---|---|
view-active | boolean | Whether the menu view is currently open. |
MagicMenuContent
The floating content panel for a menu view.
Props
| Prop | Type | Required |
|---|---|---|
boolean | false | |
string | false | |
{ to: string | RendererElement, disabled?: boolean } | false | |
Middleware[] | false | |
HTMLElement | ComponentPublicInstance | false |
MagicMenuItem
A single interactive item inside a menu.
Props
| Prop | Type | Required |
|---|---|---|
string | false | |
boolean | false |
Slot Props
| Prop | Type | Description |
|---|---|---|
item-active | boolean | Whether the item is currently selected. |
item-disabled | boolean | Whether the item is currently disabled. |
MagicMenuChannel
A conditional container that renders when its associated channel is active.
Props
| Prop | Type | Required |
|---|---|---|
MaybeRef<string> | false | |
string | false |
MagicMenuRemote
Activates a specific menu channel on click.
Props
| Prop | Type | Required |
|---|---|---|
string | true | |
string | false | |
string | false | |
boolean | false | |
false | ||
boolean | false |
Slot Props
| Prop | Type | Description |
|---|---|---|
channel-active | boolean | Whether the associated channel is currently active. |
remote-disabled | boolean | Whether the remote is currently disabled. |
MagicMenuTrigger
Opens or closes the menu on click.
Props
| Prop | Type | Required |
|---|---|---|
boolean | false | |
string | false | |
string | false | |
false | ||
boolean | false |
Slot Props
| Prop | Type | Description |
|---|---|---|
view-active | boolean | Whether the menu is currently open. |
trigger-disabled | boolean | Whether the trigger is currently disabled. |
Errors
| Source | Error Code | Message |
|---|---|---|
useMenuItem | view_id_not_found | View {viewId} not found |
useMenuKeyListener | menu_not_active | MagicMenu {state.id} is not active |
MagicMenuItem | missing_instance_id | MagicMenuItem must be nested inside MagicMenuProvider |
MagicMenuItem | missing_view_id | MagicMenuItem must be nested inside MagicMenuView |
MagicMenuItem | missing_content_id | MagicMenuItem must be nested inside MagicMenuContent |
MagicMenuTrigger | missing_instance_id | MagicMenuTrigger must be nested inside MagicMenuProvider |
MagicMenuTrigger | missing_view_id | MagicMenuTrigger must be nested inside MagicMenuView |
MagicMenuContent | missing_instance_id | MagicMenuContent must be nested inside MagicMenuProvider |
MagicMenuContent | missing_view_id | MagicMenuContent must be nested inside MagicMenuView |
MagicMenuRemote | missing_instance_id | MagicMenuRemote must be nested inside MagicMenuProvider |
MagicMenuRemote | missing_view_id | MagicMenuRemote must be nested inside MagicMenuView |
MagicMenuRemote | id_required | id is required to select an item |
MagicMenuView | missing_instance_id | MagicMenuView must be nested inside MagicMenuProvider |
Examples
The menu includes four different modes which preconfigure its appeareance and behavior. You can set the mode via the options.mode prop on the MagicMenuProvider.
Menu Bar
A menu common in desktop applications which provides top-level as well as nested commands. Its behavior is modeled after the MacOS Finder.
<template>
<magic-menu-provider
id="magic-menu-menubar-demo"
class="bg-surface-high inline-flex gap-2 rounded-2xl p-1"
>
<magic-menu-view
v-for="(menuItem, index) in menu"
:id="`magic-menu-menubar-demo-view-${menuItem.label}`"
:key="index"
>
<magic-menu-trigger v-slot="{ viewActive }" as-child>
<m-button :mode="viewActive ? 'translucent' : 'ghost'" size="xs">
{{ menuItem.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content :middleware="offsetMiddleware">
<div class="bg-surface-high w-[220px] rounded-2xl p-1">
<nested-demo-menu
v-for="(item, itemIndex) in menuItem.items"
:key="itemIndex"
:item="item"
/>
</div>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</template>
<script lang="ts" setup>
import { offset } from '@floating-ui/dom'
import { MButton } from '@maas/mirror/vue'
import NestedDemoMenu from './components/NestedDemoMenu.vue'
import '@maas/vue-equipment/utils/css/keyframes/fade-in.css'
import '@maas/vue-equipment/utils/css/keyframes/fade-out.css'
const offsetMiddleware = [offset({ crossAxis: -4, mainAxis: 8 })]
const menu = [
{
label: 'Edit',
type: 'button',
items: [
{ label: 'Undo', cmd: '⌘ Z' },
{ label: 'Redo', cmd: '⇧ ⌘ Z' },
{
label: 'Find',
items: [
{ label: 'Search the web…' },
{ label: 'Find…' },
{ label: 'Find Next' },
{ label: 'Find Previous' },
],
},
{ label: 'Cut', cmd: '⌘ X' },
{ label: 'Copy', cmd: '⌘ C' },
{ label: 'Paste', cmd: '⌘ V' },
],
},
{
label: 'File',
type: 'ghost',
items: [
{ label: 'New Tab', cmd: '⌘ T' },
{ label: 'New Window', cmd: '⌘ W' },
{ label: 'New Incognito Window', disabled: true },
{
label: 'Share',
items: [
{ label: 'Email Link', disabled: true },
{ label: 'Messages' },
{ label: 'Notes' },
{
label: 'Socials',
items: [{ label: 'Instagram' }, { label: 'Bluesky' }],
},
],
},
{ label: 'Print', cmd: '⌘ P' },
],
},
{
label: 'View',
type: 'ghost',
items: [
{ label: 'Show Bookmarks', cmd: '⇧ ⌘ B' },
{ label: 'Show Full URLs' },
{ label: 'Reload', cmd: '⌘ R' },
{ label: 'Force Reload', cmd: '⇧ ⌘ R' },
{ label: 'Fullscreen', cmd: '⌘ F' },
{ label: 'Hide Sidebar' },
],
},
{
label: 'Profiles',
type: 'ghost',
items: [
{
label: 'Christoph Jeworutzki',
items: [
{ label: 'User Settings' },
{ label: 'Edit Profile' },
{ label: 'Subscribtions' },
],
},
{
label: 'Robin Scholz',
items: [
{ label: 'User Settings' },
{ label: 'Edit Profile' },
{ label: 'Subscribtions' },
],
},
{ label: 'Edit…' },
{ label: 'Add Profile…' },
],
},
]
</script>Navigation Bar
A collection of links for navigating websites.
<template>
<div
class="border-surface bg-surface-high relative flex h-15 items-center gap-1 overflow-hidden rounded-[1.25rem] p-1"
>
<magic-menu-provider
v-if="menu"
id="navigation-bar-demo"
:options="{
mode: 'navigation',
}"
class="flex"
>
<magic-menu-view id="navigation-bar-demo-view" ref="view">
<div class="flex gap-1">
<magic-menu-trigger
v-for="(item, i) in menu"
:key="i"
as-child
class="ui-menu-button"
>
<magic-menu-remote
v-slot="{ channelActive }"
:channel-id="item.id"
as-child
>
<m-button :mode="channelActive ? 'translucent' : 'ghost'">
<span class="flex items-center gap-2.5">
<span>{{ item.label }}</span>
<m-badge
v-if="item.badge"
mode="outline"
variant="primary"
size="sm"
>
{{ item.badge }}
</m-badge>
</span>
</m-button>
</magic-menu-remote>
</magic-menu-trigger>
</div>
<magic-menu-content :reference-el="viewRef?.$el">
<div class="p-1 pt-2">
<m-menu-box class="navigation-bar__menu-box overflow-hidden">
<auto-size :duration="100">
<magic-menu-channel
v-for="(item, i) in menu"
:id="item.id"
:key="i"
class="relative inline-flex gap-4"
>
<div
v-for="(entry, j) in item.lists"
:key="j"
class="w-[16rem]"
>
<div
v-if="entry.label"
class="flex items-center gap-2 pt-4 pb-2 pl-7"
>
<span class="type-surface-callout-sm text-surface-muted">
{{ entry.label }}
</span>
<m-badge v-if="'badge' in entry" size="xs" mode="tone">
{{ entry.badge }}
</m-badge>
</div>
<menu-demo-card
v-for="(data, k) in entry.list"
:key="k"
:data="data"
/>
</div>
</magic-menu-channel>
</auto-size>
</m-menu-box>
</div>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</div>
</template>
<script lang="ts" setup>
import { useTemplateRef } from 'vue'
import { AutoSize } from '@maas/vue-autosize'
import { MBadge, MButton, MMenuBox } from '@maas/mirror/vue'
import MenuDemoCard from './components/MenuDemoCard.vue'
import type { ComponentPublicInstance } from 'vue'
const viewRef = useTemplateRef<ComponentPublicInstance>('view')
const menu = [
{
label: 'Catalogue',
id: 'catalogue-channel',
lists: [
{
label: 'Commercial',
list: [
{
label: 'Mirror Ui',
callout: 'Interface System',
icon: 'maas-mr',
},
{
label: 'Dreamtype™',
badge: 'Soon',
callout: 'Commercial Fonts',
icon: 'maas-dt',
},
{
label: 'Azzets',
badge: 'Soon',
callout: 'Visual Content App',
icon: 'maas-az',
},
],
},
{
label: 'Open Source',
badge: 'OSS',
list: [
{
label: 'Vue Equipment',
callout: 'Open Source Plugins',
icon: 'maas-ve',
},
{
label: 'Open Foundry',
callout: 'Open Source Fonts',
icon: 'maas-of',
},
],
},
],
},
{
label: 'Resources',
id: 'resources-channel',
lists: [
{
label: 'Company',
list: [
{
label: 'Readme',
badge: 'Blog',
callout: 'Written by MaaS',
icon: 'edit-alt',
},
{
label: 'About us',
callout: 'Who we Are',
icon: 'maas-robot',
},
],
},
{
label: 'Community',
id: 'community-channel',
list: [
{
icon: 'brand-github',
label: 'GitHub',
callout: 'What we Code',
},
{
icon: 'brand-figma',
label: 'Figma',
badge: 'Soon',
callout: 'Design Resources',
},
],
},
{
label: 'Packages',
badge: 'OSS',
list: [
{
icon: 'brand-vue',
label: 'Vue Primitive',
callout: '@maas/vue-primitive',
},
{
icon: 'brand-nuxt',
label: 'MagicImage',
callout: '@maas/magic-image',
},
],
},
],
},
{
label: 'Enterprise',
id: 'enterprise-channel',
badge: 'Pro',
lists: [
{
label: 'Solutions',
list: [
{
label: 'Mirror Ui',
callout: 'Interface System',
icon: 'maas-mr',
},
{
label: 'Vue Equipment',
callout: 'Frontend Toolkit',
icon: 'maas-ve',
},
],
},
{
label: 'Group',
list: [
{
label: 'International Magic',
callout: 'Creative Studio',
},
{
label: 'ONE',
callout: 'Production Collective',
},
],
},
],
},
]
</script>
<style>
.navigation-bar__menu-box {
--menu-box-box-shadow: none;
--menu-box-color-bg: var(--app-color-surface-bg-high);
}
</style>Dropdown Menu
A single top level menu which provides top-level as well as nested commands, triggered by a click, anchored at a reference element, like a button.
<template>
<magic-menu-provider
id="magic-menu-dropdown-demo"
:options="{ mode: 'dropdown' }"
>
<magic-menu-view id="magic-menu-dropdown-demo-view">
<magic-menu-trigger as-child>
<m-button size="xs">
{{ menu.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content>
<div class="bg-surface-high w-[220px] rounded-2xl p-1 text-black">
<nested-demo-menu
v-for="(item, itemIndex) in menu.items"
:key="itemIndex"
:item="item"
/>
</div>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</template>
<script lang="ts" setup>
import { MButton } from '@maas/mirror/vue'
import NestedDemoMenu from './components/NestedDemoMenu.vue'
import '@maas/vue-equipment/utils/css/animations/fade-up.css'
const menu = {
label: 'Menu',
type: 'button',
items: [
{ label: 'New Tab', cmd: '⌘ T' },
{ label: 'New Window', cmd: '⌘ W' },
{ label: 'New Incognito Window', disabled: true },
{
label: 'Share',
items: [
{ label: 'Email Link', disabled: true },
{ label: 'Messages' },
{ label: 'Notes' },
{
label: 'Socials',
items: [{ label: 'Instagram' }, { label: 'Bluesky' }],
},
],
},
{ label: 'Print', cmd: '⌘ P' },
{ label: 'Show Bookmarks', cmd: '⇧ ⌘ B' },
{ label: 'Show Full URLs' },
{ label: 'Reload', cmd: '⌘ R' },
{ label: 'Force Reload', cmd: '⇧ ⌘ R' },
{ label: 'Fullscreen', cmd: '⌘ F' },
{ label: 'Hide Sidebar' },
],
}
</script>
<style>
:root {
--magic-menu-float-arrow-color: var(--app-color-surface-bg-high);
}
</style>Context Menu
A single top level menu which provides top-level as well as nested commands, triggered by a right click, anchored at the pointer event coordinates.
<template>
<magic-menu-provider
id="magic-menu-context-demo"
:options="{ mode: 'context' }"
class="w-full"
>
<magic-menu-view id="magic-menu-context-view">
<magic-menu-trigger
class="border-surface flex h-36 w-full items-center justify-center rounded-lg border-2 border-dashed"
>
<span class="type-control-text-sm text-surface-subtle">
Right Click
</span>
</magic-menu-trigger>
<magic-menu-content>
<div class="bg-surface-high w-[220px] rounded-2xl p-1 text-black">
<nested-demo-menu
v-for="(item, itemIndex) in menu.items"
:key="itemIndex"
:item="item"
/>
</div>
</magic-menu-content>
</magic-menu-view>
</magic-menu-provider>
</template>
<script lang="ts" setup>
import NestedDemoMenu from './components/NestedDemoMenu.vue'
const menu = {
label: 'Menu',
type: 'button',
items: [
{ label: 'New Tab', cmd: '⌘ T' },
{ label: 'New Window', cmd: '⌘ W' },
{ label: 'New Incognito Window', disabled: true },
{
label: 'Share',
items: [
{ label: 'Email Link', disabled: true },
{ label: 'Messages' },
{ label: 'Notes' },
{
label: 'Socials',
items: [{ label: 'Instagram' }, { label: 'Bluesky' }],
},
],
},
{ label: 'Print', cmd: '⌘ P' },
{ label: 'Show Bookmarks', cmd: '⇧ ⌘ B' },
{ label: 'Show Full URLs' },
{ label: 'Reload', cmd: '⌘ R' },
{ label: 'Force Reload', cmd: '⇧ ⌘ R' },
{ label: 'Fullscreen', cmd: '⌘ F' },
{ label: 'Hide Sidebar' },
],
}
</script>