Vue Final Modal
Guide

Migration Guide

This document should help you navigate the tricky path of migrating between different versions of vue-final-modal that introduced breaking changes.

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: stable where the browser supports it. The page still gets the scrollbar's width as padding-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.

server entry
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:

nuxt.config.ts
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, props and emits of useModal() accept a ref, computed or 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 to useModalSlot()).
  • createModalsProvider() creates an isolated instance with its own <ModalsProvider>, useModal() and useVfm() for advanced scenarios such as micro frontends.
  • The promises of open() and close(), from useModal() and from useVfm(), always settle: with 'opened' or 'closed' once the transition has finished, or with a message when the modal never got there, for example when beforeOpen called stop() or the modal was destroyed first. See what they resolve with.
  • require('vue-final-modal') loads a CommonJS build with its own .d.cts types instead of the UMD build. The UMD build is still there for CDNs.

Summary

4.x5.0
createVfm() + app.use(vfm)Unchanged
<ModalsContainer />Unchanged
useModal().open() / .close()Unchanged
defaultModelValue / keepAliveUnchanged
useModalSlot()Unchanged (defineTemplate() is the new alternative)
this.$vfm / useVfm()Unchanged
vfm.dynamicModalsRemoved, use the functions returned by useModal()
useModal().optionsRemoved, use reactive attrs
patchOptions()Removed, use reactive attrs (ref / computed / getter)
getModalExposed()Removed, use useVfm()
Server open() outside any app contextIgnored with a warning
open() from a Nuxt plugin or route middlewareOpens 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 composablesAuto-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.

Copyright © 2026