Migration Guide
Migrating from 4.x to 5.0
vue-final-modal 5.0 rebuilds useModal() on top of vue-use-template, while keeping the public API compatible with 4.x.
For most apps no code change is needed: createVfm(), <ModalsContainer />, useModal() with open() / close(), useVfm(), useModalSlot() and $vfm all keep working as before. The <VueFinalModal> component itself (props, events, slots) stays the same.
What can need a change: installing the peer dependencies, a few removed advanced APIs that leaked the old internals, and some behaviors described below.
Install the peer dependencies
4.x installed @vueuse/core, @vueuse/integrations and focus-trap as its own dependencies. 5.0 lists them only as peer dependencies, together with the new vue-use-template, and needs Vue 3.3 or later instead of 3.2. npm and pnpm install peer dependencies for you. With Yarn, add them yourself:
yarn add vue-final-modal vue-use-template @vueuse/core @vueuse/integrations focus-trap
Removed vfm.dynamicModals
The dynamic modals created by useModal() are no longer exposed as an array on the vfm instance. Control each modal with the open() / close() functions returned by useModal(), or close everything with useVfm().closeAll().
Removed useModal().options and patchOptions()
useModal() no longer returns the internal reactive options object, and patchOptions() is gone. Pass a ref, computed or getter function as attrs instead. The modal stays in sync with it:
So this:
const modal = useModal({
component: ModalConfirm,
attrs: { title: 'Hello World!' },
})
modal.patchOptions({ attrs: { title: 'Hello Vue!' } })
Will be re-written as this:
const title = ref('Hello World!')
const { open, close } = useModal({
component: ModalConfirm,
attrs: () => ({ title: title.value }),
})
title.value = 'Hello Vue!'
Swapping the component of an existing modal is no longer supported: create a separate modal with useModal() instead.
Removed getModalExposed()
getModalExposed() operated on component internal instances and is gone. useVfm() keeps exposing get(modalId), open(modalId), close(modalId), toggle(modalId) and closeAll().
focusTrap options are merged over the defaults
In 4.x, the options you passed to focusTrap replaced its default { allowOutsideClick: true }. In 5.0 they are merged over { allowOutsideClick: true, escapeDeactivates: false }, so outside clicks keep reaching clickToClose unless you set allowOutsideClick yourself. The esc key no longer deactivates the focus trap on its own: escToClose handles it, so with escToClose: false the focus stays trapped in the modal.
Scroll locking moved to @hunterliu/scroll-lock
5.0 locks the scroll with @hunterliu/scroll-lock, which replaces the copy of body-scroll-lock that 4.x carried. lockScroll and reserveScrollBarGap keep their names and defaults. Two things you may notice:
- A modal rendered inside a scroll container, for example with
teleportTo: false, locks that container as well as the page. - A scroll container keeps the room of its scrollbar with
scrollbar-gutter: stablewhere the browser supports it. The page still gets the scrollbar's width aspadding-right.
open() outside any app context on the server is ignored
On the server, open() of a modal created outside a component (for example at module level or in a store) and called outside any app context is now ignored with a warning. 4.x opened it in the last created vfm, which can belong to another request when requests run concurrently. Call useModal() in setup() to open a modal during a server render. In the browser nothing changes.
A modal opened outside a component opens once the app has mounted
A modal opened outside a component before the app renders, for example from a Nuxt plugin or a route middleware, is no longer part of the server HTML: the server skips that open() with a warning and resolves it right away. In the browser the modal opens once the app has mounted, with its enter transition, so hydration never meets a modal the server did not render, whether the code runs on both sides or only in the browser. 4.x rendered such a modal on the server, and one opened only in the browser before the app mounted caused a hydration mismatch. Do not await that open() before the app mounts: it settles once the modal has opened. To show a modal on the first paint, open it while a component sets up, for example in app.vue or in a page.
A server that polyfills window cannot be told apart from a browser by looking at window, so it has to say it is a server: call markServer() once, before it renders. The Nuxt module does this for you.
import { markServer } from 'vue-final-modal'
markServer()
The Nuxt module ships inside the package
@vue-final-modal/nuxt is no longer published. The module is part of vue-final-modal as vue-final-modal/nuxt, so remove the old package and change the module name:
export default defineNuxtConfig({
modules: ['vue-final-modal/nuxt'],
})
The module now also auto-imports the components and composables. If another module or your own composables/ directory auto-imports one of the same names, such as a useModal() of its own, set vueFinalModal: { autoImports: false } and keep importing from vue-final-modal. See Setup.
New capabilities
Not breaking, but worth knowing:
attrs,propsandemitsofuseModal()accept aref,computedor getter for reactive updates.- A modal opened while a component sets up during the server render is part of the server HTML, with its z-index, and hydrates already open: no enter transition plays on the first paint.
defineTemplate()is a type helper for slot content with props and events (an alternative touseModalSlot()).createModalsProvider()creates an isolated instance with its own<ModalsProvider>,useModal()anduseVfm()for advanced scenarios such as micro frontends.- The promises of
open()andclose(), fromuseModal()and fromuseVfm(), always settle: with'opened'or'closed'once the transition has finished, or with a message when the modal never got there, for example whenbeforeOpencalledstop()or the modal was destroyed first. See what they resolve with. require('vue-final-modal')loads a CommonJS build with its own.d.ctstypes instead of the UMD build. The UMD build is still there for CDNs.
Summary
| 4.x | 5.0 |
|---|---|
createVfm() + app.use(vfm) | Unchanged |
<ModalsContainer /> | Unchanged |
useModal().open() / .close() | Unchanged |
defaultModelValue / keepAlive | Unchanged |
useModalSlot() | Unchanged (defineTemplate() is the new alternative) |
this.$vfm / useVfm() | Unchanged |
vfm.dynamicModals | Removed, use the functions returned by useModal() |
useModal().options | Removed, use reactive attrs |
patchOptions() | Removed, use reactive attrs (ref / computed / getter) |
getModalExposed() | Removed, use useVfm() |
Server open() outside any app context | Ignored with a warning |
open() from a Nuxt plugin or route middleware | Opens in the browser once the app has mounted |
modules: ['@vue-final-modal/nuxt'] | modules: ['vue-final-modal/nuxt'], no extra package |
| Nuxt: import the components and composables | Auto-imported, vueFinalModal: { autoImports: false } to opt out |
@vueuse/* and focus-trap as dependencies, Vue 3.2+ | Peer dependencies with vue-use-template (add them yourself with Yarn), Vue 3.3+ |
focusTrap options replace { allowOutsideClick: true } | Merged over { allowOutsideClick: true, escapeDeactivates: false } |
| Vendored body-scroll-lock for the page | @hunterliu/scroll-lock, also locks the scroll container a modal is rendered in |
Migrating from 3.x to 4.0
Please check the vue-final-modal 4.x documentation for the 3.x to 4.0 migration guide, then migrate to 5.0 from there.