Resources

FAQ

Frequently asked questions about @kolirt/vue-modal v2.

FAQ

Why <ModalTarget> instead of plain <Teleport>?

<Teleport> moves DOM nodes but provides no coordination. <ModalTarget> is the stack manager for one modal group: it filters state.modals to its group, drives the enter/exit transition sequence (one modal animates out before the next becomes visible), owns scroll-lock and user-select lock for that group, and provides the modalGroupConfigKey injection that <ModalRoot> and useModalContext depend on. Multiple <ModalTarget> instances give you independently positioned stacks (e.g., full-screen dialogs at one target, side-panels at another).

Does it work with SSR?

The component primitives (<ModalRoot>, <ModalContent>, etc.) render on the server without errors because <ModalTarget> uses a plain <div> with no DOM-only APIs until mounted. However, openModal is imperative — it mutates reactive state and is meaningless on the server. Only call it inside event handlers or onMounted. Wrapping calls in if (typeof window !== 'undefined') is not necessary for the package itself, but do not call openModal at module scope.

How do I test modals?

Integration tests (recommended). Mount the full app (or a test-app wrapper that registers the plugin and places <ModalTarget>), then call openModal in the test body and query the rendered output with Testing Library.

Unit tests for a modal component. Wrap the component in a <ModalTarget group="…"> stub that provides the required injections, or use openModal after mounting a minimal app in jsdom. Avoid mounting <ModalRoot> in isolation — it throws if modalContextKey is not injected.

See the Confirm dialog recipe for a concrete example.

Why is there no <Teleport> per modal?

<ModalTarget> is effectively the teleport target — it's position: fixed; inset: 0 by default and sits wherever you place it in the DOM. Each modal rendered inside it is already in the correct stacking layer. Adding a <Teleport> per modal would teleport elements into a target that is itself fixed-positioned, which gains nothing and breaks the group isolation <ModalTarget> provides.

Can I use this with Vue 2?

No. The package requires Vue 3.4+ and relies on provide/inject, <script setup>, and reactivity APIs that are not available in Vue 2.

Why does reka-ui appear as a peer dependency?

<ModalRoot> wraps reka-ui's <DialogRoot> to get ARIA role="dialog", the focus trap, and Escape key handling correct out of the box. reka-ui is itself dependency-light and tree-shakeable; only the DialogRoot primitive is consumed. Because reka-ui ships multiple composables your app may already use, it is declared as a peer dep rather than bundled.

How does scroll-lock work under iOS?

useScrollLock sets document.body.style.overflow = 'hidden' and compensates for the removed scrollbar by adding paddingRight equal to window.innerWidth - document.documentElement.clientWidth. This prevents layout shift. iOS Safari's elastic overscroll is not fully blocked by overflow alone — if you need to prevent body bounce, add -webkit-overflow-scrolling: touch to the modal's inner scroll container and set overscroll-behavior: contain.

Can I open a modal during SSR or hydration?

No. openModal pushes to state.modals which triggers reactive renders — this must happen client-side. Guard calls with onMounted if your component setup runs during SSR:

import { onMounted } from 'vue'
import { openModal } from '@kolirt/vue-modal'

onMounted(() => {
  openModal(WelcomeDialog, { group: 'default' })
})

How do I get the currently open top modal programmatically?

Import modals from the package (a computed ref of ModalItem[]) and read the last element:

import { modals } from '@kolirt/vue-modal'

const topModal = computed(() => modals.value[modals.value.length - 1])

For group-scoped top-of-stack, use groupModals:

import { groupModals } from '@kolirt/vue-modal'

const confirmModals = groupModals('confirm')
const topConfirm = computed(() => confirmModals.value[confirmModals.value.length - 1])

See the State API reference.

Can I prevent a modal from closing?

Yes — register a beforeClose guard with useModalContext().onBeforeClose:

const modal = useModalContext()

modal.onBeforeClose(async () => {
  if (formIsDirty.value) {
    const confirmed = await openModal(ConfirmDiscard, { group: 'confirm' }).catch(() => false)
    if (!confirmed) return false // veto the close
  }
})

Return false (or a promise that resolves to false) to block the close. Guards are bypassed when ignoreGuard: true is passed.

Copyright © 2026