FAQ
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.
