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-base 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 ? 'subtle' : 'ghost'" size="xs">
{{ menuItem.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content :middleware="offsetMiddleware">
<div class="bg-surface-base 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 'mirror-compat'
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>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 add @maas/vue-equipment
Vue
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')
}
Peer Dependencies
MagicMenu relies on the following optional peer dependencies in addition to the core peer dependencies. Package managers don’t install optional peer dependencies automatically, so add them to your project.
Installation
pnpm add @floating-ui/vue
API Reference
MagicMenuProvider
The MagicMenuProvider wraps the menu and configures all child components according to the provided options.
Props
Options
To customize the menu, override the necessary options. Any custom options will be merged with the default options.
MagicMenuView
Groups menu items into a single navigation level and tracks its active state.
Props
Slot Props
MagicMenuContent
The floating content panel for a menu view.
Props
MagicMenuItem
A single interactive item inside a menu.
Props
Slot Props
MagicMenuChannel
A conditional container that renders when its associated channel is active.
Props
MagicMenuRemote
Activates a specific menu channel on click.
Props
Slot Props
MagicMenuTrigger
Opens or closes the menu on click.
Props
Slot Props
Errors
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-base 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 ? 'subtle' : 'ghost'" size="xs">
{{ menuItem.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content :middleware="offsetMiddleware">
<div class="bg-surface-base 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 'mirror-compat'
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-base 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 ? 'subtle' : '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"
variant="secondary"
>
{{ 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 'mirror-compat'
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 {
--m-menu-box-shadow: none;
background-color: var(--app-color-surface-bg-base);
}
</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-base w-[220px] rounded-2xl p-1">
<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 'mirror-compat'
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-base);
}
</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-muted">
Right Click
</span>
</magic-menu-trigger>
<magic-menu-content>
<div class="bg-surface-base w-[220px] rounded-2xl p-1">
<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>