Concepts

Imperative flow

Why the API is promise-based and how the modal lifecycle works.

Imperative flow

Modals are opened from code and awaited like any other async call. Result, cancellation, and chained logic stay in one function.

Why promises, not v-model

A v-model-style API splits a modal's outcome across props, refs, and event handlers. With a promise, the entire flow stays in one place:

const name = await openModal<string>(NameDialog).catch(() => null)
if (!name) return
await api.rename(id, name)

The modal call is a function call that happens to render UI. The caller awaits it, reads the result, and continues.

Lifecycle

openModal(Component, options)
  │
  ├── component mounts inside its group target
  │   user interacts …
  │
  ├── confirm(data)   → exit animation → promise resolves with data
  └── close() / Esc / overlay → exit animation → promise rejects (ModalClosedError)

The promise settles after the exit animation finishes, so your CSS transition always plays out. Pass instantExit: true to skip it.

Inside the modal

useModalContext<T>() exposes confirm(data) and close():

ConfirmDialog.vue
<script setup lang="ts">
import { ModalRoot, ModalContent, useModalContext } from '@kolirt/vue-modal'

defineOptions({ modalGroup: 'default' })

const { close, confirm } = useModalContext<boolean>()
</script>

<template>
  <ModalRoot>
    <ModalContent>
      <button @click="close()">Cancel</button>
      <button @click="confirm(true)">OK</button>
    </ModalContent>
  </ModalRoot>
</template>
  • confirm(data) — resolves the promise with data.
  • close() — rejects with ModalClosedError.

On the calling side

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

// Inline try/catch — explicit success vs cancel
try {
  const ok = await openModal<boolean>(ConfirmDialog, {
    props: { message: 'Delete this item?' }
  })
  if (ok) await api.delete(id)
} catch (e) {
  if (!(e instanceof ModalClosedError)) throw e
}

// .catch() shorthand — collapse cancel to a falsy value
const ok = await openModal<boolean>(ConfirmDialog, {
  props: { message: 'Delete this item?' }
}).catch(() => false)

if (ok) await api.delete(id)

Chaining

Because openModal returns a real promise, multi-step flows stay linear:

async function rename() {
  const name = await openModal<string>(NameDialog).catch(() => null)
  if (!name) return

  const ok = await openModal<boolean>(ConfirmDialog, {
    props: { message: `Rename to "${name}"?` }
  }).catch(() => false)

  if (ok) await api.rename(id, name)
}

One try/catch wraps the whole sequence. No shared state stuck between handlers.

replaceModal

replaceModal(Component, options) closes the topmost modal of the target group instantly (skipping its exit animation and ignoring guards), then opens a new one. Use it for wizard-style step swaps where the previous step's exit animation would feel slow.

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

replaceModal(StepTwo, {
  props: { data: collected },
  instantEnter: true
})

The previous step's promise rejects with ModalClosedError — make sure the original caller catches it. See Opening & closing for the full action reference.

Copyright © 2026