MagicModal
MagicModal is a flexible, unstyled modal component. Useful for things like confirmation dialogs, detail views, and forms.
<template>
<m-button @click="modalApi.open">Open Modal</m-button>
<magic-modal id="magic-modal-default-demo" :options="{ focusTrap: false }">
<div class="bg-surface-high aspect-4/3 w-[40rem] rounded-md" />
</magic-modal>
</template>
<script lang="ts" setup>
import { MButton } from 'mirror-compat'
import { useMagicModal } from '@maas/vue-equipment/plugins/MagicModal'
const modalApi = useMagicModal('magic-modal-default-demo')
</script>Anatomy
MagicModal can be used as a single self-contained component or composed from its individual parts for full control.
Simple
<template>
<magic-modal id="your-modal-id">
<!-- your content -->
</magic-modal>
</template>
<script setup>
const { open } = useMagicModal('your-modal-id')
</script>
Composed
<template>
<magic-modal-provider id="your-modal-id">
<magic-modal-trigger as-child>
<button>Open</button>
</magic-modal-trigger>
<magic-modal-teleport>
<magic-modal-backdrop />
<magic-modal-content>
<!-- your content -->
</magic-modal-content>
</magic-modal-teleport>
</magic-modal-provider>
</template>
Installation
CLI
Add @maas/vue-equipment to your dependencies.
pnpm add @maas/vue-equipment
Vue
If you are using Vue, import and add MagicModalPlugin to your app.
import { createApp } from 'vue'
import { MagicModalPlugin } from '@maas/vue-equipment/plugins/MagicModal'
const app = createApp({})
app.use(MagicModalPlugin)
Nuxt
The modal is available as a Nuxt module. In your Nuxt config file add @maas/vue-equipment/nuxt to your modules and add MagicModal to the plugins in your configuration.
export default defineNuxtConfig({
modules: ['@maas/vue-equipment/nuxt'],
vueEquipment: {
plugins: ['MagicModal'],
},
})
Direct Import
If you prefer a more granular approach, components can be directly imported.
<script setup>
import {
MagicModalProvider,
MagicModalTeleport,
MagicModalBackdrop,
MagicModalContent,
MagicModalTrigger,
} from '@maas/vue-equipment/plugins/MagicModal'
</script>
Composable
In order to interact with the modal from anywhere within your app, we provide a useMagicModal composable. Import it directly when needed.
import { onMounted } from 'vue'
import { useMagicModal } from '@maas/vue-equipment/plugins/MagicModal'
const { open } = useMagicModal('your-modal-id')
onMounted(() => {
open()
})
Peer Dependencies
MagicModal 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 @vueuse/integrations focus-trap
API Reference
MagicModalProvider
The MagicModalProvider wraps the modal and configures all child components according to the provided options.
Props
Options
To customize the modal, override the necessary options. Any custom options will be merged with the default options.
MagicModalTeleport
Teleports all child components to a target in the DOM. Uses v-if to mount and unmount its contents when the modal opens and closes — keeping the DOM clean when the modal is inactive. Omit this component if you want the modal to remain mounted at all times.
Props
MagicModalBackdrop
Renders a full-viewport overlay behind the modal panel. Closes the modal when clicked.
CSS Variables
MagicModalContent
Renders the modal panel with focus trap and scroll lock support.
CSS Variables
MagicModalTrigger
Opens or closes the modal on click.
Props
Slot Props
MagicModal
A self-contained component that composes all primitives internally. Use this for simple cases where you don’t need to customise the markup.
Props
Slots
Errors
Caveats
Starting with iOS 26, Safari renders page content beneath its translucent UI chrome, while position: fixed elements can stop short of the bottom controls. A fixed backdrop may leave a strip of undimmed content visible behind the chrome. To cover everything on screen, switch the backdrop to position: absolute and give it a positioned ancestor:
body {
position: relative;
margin: 0;
}
@supports (-webkit-touch-callout: none) {
:root {
--magic-modal-backdrop-position: absolute;
--magic-modal-backdrop-height: max(100%, 100dvh);
}
}
The @supports query limits the override to iOS, where every browser runs WebKit. The height override keeps the backdrop covering the entire document on scrolled pages, while never shrinking below the viewport on short ones. Leave --magic-modal-position untouched — the content needs position: fixed to stay centered in the viewport.
The pattern relies on body spanning the full document and does not cover pages that scroll horizontally. If you teleport the modal somewhere other than body, the new target needs to be the positioned ancestor instead.
Examples
Default
<template>
<m-button @click="modalApi.open">Open Modal</m-button>
<magic-modal id="magic-modal-default-demo" :options="{ focusTrap: false }">
<div class="bg-surface-high aspect-4/3 w-[40rem] rounded-md" />
</magic-modal>
</template>
<script lang="ts" setup>
import { MButton } from 'mirror-compat'
import { useMagicModal } from '@maas/vue-equipment/plugins/MagicModal'
const modalApi = useMagicModal('magic-modal-default-demo')
</script>Composed
<template>
<magic-modal-provider
id="magic-modal-composed-demo"
:options="{ focusTrap: false }"
>
<magic-modal-trigger as-child>
<m-button>Open Modal</m-button>
</magic-modal-trigger>
<magic-modal-teleport>
<magic-modal-backdrop />
<magic-modal-content>
<div class="bg-surface-high aspect-[4/3] w-[40rem] rounded-md" />
</magic-modal-content>
</magic-modal-teleport>
</magic-modal-provider>
</template>
<script lang="ts" setup>
import { MButton } from 'mirror-compat'
import {
MagicModalBackdrop,
MagicModalContent,
MagicModalProvider,
MagicModalTeleport,
MagicModalTrigger,
} from '@maas/vue-equipment/plugins/MagicModal'
</script>