Guide

Modal context

Full reference for useModalContext — id, group, isTopmost, effectiveOptions, close, confirm, onBeforeClose.

Modal context

useModalContext<T>() is the primary API inside a modal component. It returns everything the component needs to interact with the modal system.

Signature

function useModalContext<T = unknown>(): {
  id: number
  group: ModalGroup
  isClosing: ComputedRef<boolean>
  isTopmost: ComputedRef<boolean>
  isTopmostGlobal: ComputedRef<boolean>
  effectiveOptions: ComputedRef<ModalEffectiveOptions>
  close(opts?: { ignoreGuard?: boolean; instantExit?: boolean }): void
  confirm(data: T, opts?: { ignoreGuard?: boolean; instantExit?: boolean }): void
  onBeforeClose(handler: BeforeCloseHandler): void
}

Throws if called outside a modal opened via openModal / useModal.

id and group

Numeric id and group name for this instance. Stable for the modal's lifetime. Pass id outward when something else needs to close this modal:

const { id } = useModalContext()
// elsewhere:
closeModalById(id, { success: true, data: result })

isClosing

true once a close is requested and the exit animation begins. The component is still mounted — use it to disable interactions during exit:

<button :disabled="isClosing" @click="confirm(true)">OK</button>

isTopmost vs isTopmostGlobal

PropertyTrue when…
isTopmostThis modal is the topmost within its own group.
isTopmostGlobalThis modal is the topmost across all groups.

Use isTopmost to gate group-local UI; isTopmostGlobal for app-wide concerns (e.g. updating document.title).

effectiveOptions

ComputedRef<ModalEffectiveOptions> — the resolved behavior for this modal, combining <ModalTarget> props and the registered group config.

interface ModalEffectiveOptions {
  interactOutside: boolean         // non-modal mode (no focus trap)
  closeOnInteractOutside: boolean  // click outside region closes
  closeOnInteractOverlay: boolean  // click on overlay area closes
  lockBodyScroll: boolean          // body scroll is locked
  closeOnEscape: boolean           // Esc closes
}

Values are in the positive form (already negated from the disable* flags). Read-only.

close(opts?)

Rejects the modal's promise with ModalClosedError. Runs onBeforeClose guards unless ignoreGuard: true.

close()
close({ ignoreGuard: true })
close({ instantExit: true })

confirm(data, opts?)

Resolves the modal's promise with data. Type must match the generic on useModalContext<T>().

const { confirm } = useModalContext<{ value: string }>()

confirm({ value: 'hello' })

close and confirm are no-ops if the modal is already closing.

onBeforeClose

Register a guard. Runs on every close path — close(), confirm(), Esc, overlay click. Return false (or Promise<false>) to veto.

type BeforeCloseHandler = () => boolean | void | Promise<boolean | void>

The guard is auto-unregistered when the component unmounts.

Dirty-form guard

EditDialog.vue
<script setup lang="ts">
import { ref } from 'vue'
import { openModal, useModalContext } from '@kolirt/vue-modal'
import ConfirmLeave from './ConfirmLeave.vue'

const { onBeforeClose } = useModalContext<{ saved: boolean }>()
const isDirty = ref(false)

onBeforeClose(async () => {
  if (!isDirty.value) return

  const ok = await openModal<boolean>(ConfirmLeave).catch(() => false)
  if (!ok) return false  // veto
})
</script>

Bypassing guards

Pass ignoreGuard: true from the caller side — replaceModal does this internally; route guards typically pair it with instantExit:

closeAllModals({ ignoreGuard: true, instantExit: true })
handle.close({ ignoreGuard: true })

Full example

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

defineOptions({ modalGroup: 'default' })

const props = defineProps<{ userId: number }>()

const { isClosing, close, confirm, onBeforeClose } = useModalContext<{ name: string }>()

const name = ref('')
const dirty = ref(false)

onBeforeClose(() => {
  if (dirty.value) return false
})
</script>

<template>
  <ModalRoot class="root">
    <ModalContent class="card">
      <ModalTitle>Edit profile #{{ props.userId }}</ModalTitle>
      <input v-model="name" @input="dirty = true" />
      <button :disabled="isClosing" @click="close()">Cancel</button>
      <button :disabled="isClosing" @click="confirm({ name })">Save</button>
    </ModalContent>
  </ModalRoot>
</template>
Copyright © 2026