Skip to content

MagicModal

MagicModal is a flexible, unstyled modal component. Useful for things like confirmation dialogs, detail views, and forms.

View source View as Markdown

DefaultDemo.vue
<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

PropTypeRequired
id
MaybeRef<string>true
options
MagicModalOptionsfalse

Options

To customize the modal, override the necessary options. Any custom options will be merged with the default options.

OptionTypeDefault
tag
string
'dialog'
focusTrap
boolean | FocusTrapOptionsobject
scrollLock
boolean | objectobject
scrollLock.padding
booleantrue
teleport.target
string'body'
teleport.disabled
booleanfalse
transition.content
string'magic-modal-content'
transition.backdrop
string'magic-modal-backdrop'
keyListener.close
string[] | false['Escape']

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

PropTypeRequired
to
string | RendererElementfalse
disabled
booleanfalse

MagicModalBackdrop

Renders a full-viewport overlay behind the modal panel. Closes the modal when clicked.

CSS Variables

VariableDefault
--magic-modal-backdrop-positionfixed
--magic-modal-backdrop-top0
--magic-modal-backdrop-left0
--magic-modal-backdrop-right0
--magic-modal-backdrop-bottom0
--magic-modal-backdrop-width100%
--magic-modal-backdrop-height100%
--magic-modal-backdrop-colorrgba(0, 0, 0, 0.5)
--magic-modal-backdrop-filterunset
--magic-modal-backdrop-z-index998

MagicModalContent

Renders the modal panel with focus trap and scroll lock support.

CSS Variables

VariableDefault
--magic-modal-positionfixed
--magic-modal-inset0
--magic-modal-width100%
--magic-modal-height100%
--magic-modal-displayflex
--magic-modal-justify-contentcenter
--magic-modal-align-itemscenter
--magic-modal-z-index999
--magic-modal-content-max-height100%
--magic-modal-content-width100%
--magic-modal-content-displayflex
--magic-modal-content-align-itemscenter
--magic-modal-content-justify-contentcenter
--magic-modal-content-overflow-yauto

MagicModalTrigger

Opens or closes the modal on click.

Props

PropTypeRequired
id
MaybeRef<string>false
disabled
booleanfalse
asChild
booleanfalse

Slot Props

PropTypeDescription
activebooleanWhether the modal is currently open.
disabledbooleanWhether the trigger is currently disabled.

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

PropTypeRequired
id
MaybeRef<string>true
options
MagicModalOptionsfalse

Slots

SlotDescription
defaultContent rendered inside the modal panel.
backdropContent rendered inside the backdrop element.

Errors

SourceError CodeMessage
MagicModalTeleportmissing_instance_idMagicModalTeleport must be nested inside MagicModalProvider
MagicModalBackdropmissing_instance_idMagicModalBackdrop must be nested inside MagicModalProvider
MagicModalContentmissing_instance_idMagicModalContent must be nested inside MagicModalProvider
MagicModalTriggermissing_instance_idMagicModalTrigger must be nested inside MagicModalProvider or an id must be provided

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

DefaultDemo.vue
<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

ComposedDemo.vue
<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>