Guide

Props & results

How props flow into a modal and how to type the result promise end-to-end.

Props & results

openModal accepts a typed props object and returns a typed promise. With one explicit generic on each side, the entire flow is type-checked.

Passing props

OpenModalOptions.props is typed as ExtractComponentProps<C> — derived from the component's defineProps, so TypeScript enforces the same contract as a normal usage:

UserModal.vue
<script setup lang="ts">
const props = defineProps<{
  userId: number
  readonly?: boolean
}>()
</script>
openModal(UserModal, {
  props: {
    userId: 42,      // required: number ✓
    readonly: true   // optional: boolean ✓
    // badProp: 'x'  // ✗ Type error
  }
})

Props are plain data — they are not reactive after the modal opens. For live updates, use event listeners or a shared store.

Event listeners

Pass an on map alongside props. Keys are emit event names; the package wires them through to the rendered component.

openModal(EditDialog, {
  props: { id: 42 },
  on: {
    progress: (pct: number) => console.log('progress', pct),
    'update:value': (v: string) => setValue(v)
  }
})

Inside the modal, emit normally:

<script setup lang="ts">
const emit = defineEmits<{
  progress: [pct: number]
  'update:value': [v: string]
}>()
</script>

Listeners passed to openModal live for the lifetime of that handle and are discarded when the modal closes. For listeners that survive across re-opens, use useModal.

Returning a result

The flow:

  1. openModal<TResult>(Component) returns a ModalHandle<TResult>.
  2. Inside the modal: const { confirm, close } = useModalContext<TResult>().
  3. confirm(data) → handle resolves with data.
  4. close() → handle rejects with ModalClosedError.
SaveDialog.vue
<script setup lang="ts">
import { ModalRoot, ModalContent, useModalContext } from '@kolirt/vue-modal'

defineOptions({ modalGroup: 'default' })

const props = defineProps<{ title: string }>()

interface SaveResult {
  id: number
  slug: string
}

const { confirm, close } = useModalContext<SaveResult>()

function save() {
  confirm({ id: 1, slug: 'my-post' })
}
</script>

<template>
  <ModalRoot class="root">
    <ModalContent class="card">
      <h2>{{ props.title }}</h2>
      <button @click="close()">Cancel</button>
      <button @click="save">Save</button>
    </ModalContent>
  </ModalRoot>
</template>
caller.ts
import { openModal } from '@kolirt/vue-modal'
import SaveDialog from './SaveDialog.vue'

const result = await openModal<{ id: number; slug: string }>(SaveDialog, {
  props: { title: 'Save post' }
}).catch(() => null)

if (result) {
  console.log(result.id, result.slug) // fully typed
}

The result type is not inferred from the component — annotate the same generic on both openModal<T> and useModalContext<T>.

Resolving from outside the modal

Pass { success: true, data } to any close call to resolve from the outside:

const handle = openModal<string>(MyDialog)

// later, from anywhere:
closeModalById(handle.id, { success: true, data: 'resolved externally' })

const value = await handle  // 'resolved externally'

ModalClosedError

Dismissed modals (Esc, overlay click, close()) reject with ModalClosedError. Import it to distinguish dismiss from unexpected errors:

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

try {
  await openModal(MyDialog)
} catch (e) {
  if (e instanceof ModalClosedError) {
    // normal dismiss
  } else {
    throw e
  }
}
Copyright © 2026