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-elevation-high inline-flex gap-2 rounded-2xl p-1"
>
<magic-menu-view v-for="(menuItem, index) in menu" :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-elevation-high w-[220px] rounded-2xl p-1">
<nested-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 NestedMenu from './components/NestedMenu.vue'
import { MButton } from '@maas/mirror/vue'
import { offset } from '@floating-ui/dom'
import '@maas/vue-equipment/utils/css/animations/fade-in.css'
import '@maas/vue-equipment/utils/css/animations/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-equipment
npm install @maas/vue-equipment
yarn add @maas/vue-equipment
bun install @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'
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'
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.
API 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 |
MagicMenuView
Props
Prop | Type | Required |
---|---|---|
MaybeRef<string> | false | |
false |
MagicMenuContent
Props
Prop | Type | Required |
---|---|---|
boolean | false | |
string | false | |
HTMLElement | ComponentPublicInstance | false |
MagicMenuItem
Props
Prop | Type | Required |
---|---|---|
string | false | |
boolean | false |
MagicMenuChannel
Props
Prop | Type | Required |
---|---|---|
MaybeRef<string> | false | |
string | false |
MagicMenuRemote
Props
Prop | Type | Required |
---|---|---|
string | true | |
string | false | |
string | false | |
boolean | false | |
false | ||
boolean | false |
MagicMenuTrigger
Props
Prop | Type | Required |
---|---|---|
boolean | false | |
string | false | |
string | false | |
false | ||
boolean | false |
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"
class="inline-flex gap-2 p-1 rounded-2xl bg-surface-elevation-high"
>
<magic-menu-view v-for="(menuItem, index) in menu" :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-elevation-high p-1 rounded-2xl w-[220px]">
<nested-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 NestedMenu from './components/NestedMenu.vue'
import { MButton } from '@maas/mirror/vue'
import { offset } from '@floating-ui/dom'
import '@maas/vue-equipment/utils/css/animations/fade-in.css'
import '@maas/vue-equipment/utils/css/animations/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="h-15 border-surface bg-surface-elevation-high relative flex 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 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 pb-2 pl-7 pt-4"
>
<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-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, type ComponentPublicInstance } from 'vue'
import { AutoSize } from '@maas/vue-autosize'
import { MMenuBox, MBadge, MButton } from '@maas/mirror/vue'
import MenuCard from './components/MenuCard.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: theme('backgroundColor.surface.elevation.high.DEFAULT');
}
</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" :options="{ mode: 'dropdown' }">
<magic-menu-view>
<magic-menu-trigger as-child>
<m-button size="xs">
{{ menu.label }}
</m-button>
</magic-menu-trigger>
<magic-menu-content>
<div
class="bg-surface-elevation-high text-black p-1 rounded-2xl w-[220px]"
>
<nested-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 NestedMenu from './components/NestedMenu.vue'
import { MButton } from '@maas/mirror/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>
<style>
:root {
--magic-menu-float-arrow-color: theme(
'backgroundColor.surface.elevation.high.DEFAULT'
);
}
</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"
:options="{ mode: 'context' }"
class="w-full"
>
<magic-menu-view>
<magic-menu-trigger
class="w-full h-36 border-2 border-dashed border-surface rounded-lg flex items-center justify-center"
>
<span class="type-control-text-sm text-surface-subtle">
Right Click
</span>
</magic-menu-trigger>
<magic-menu-content>
<div
class="bg-surface-elevation-high text-black p-1 rounded-2xl w-[220px]"
>
<nested-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 NestedMenu from './components/NestedMenu.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>