Getting Started

First modal

Write a modal component, open it imperatively, handle the result.

First modal

Writing a modal component

A modal component is a regular Vue component. Wrap your markup in <ModalRoot> and <ModalContent>. Use useModalContext<T>() to access close() and confirm(data) — these reject and resolve the promise returned by openModal. The argument you pass to confirm(...) becomes the resolved value of that promise.

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

defineOptions({ modalGroup: 'default' })

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

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

<template>
  <ModalRoot class="root">
    <ModalContent class="card">
      <ModalTitle>Confirm</ModalTitle>
      <ModalDescription>{{ props.message }}</ModalDescription>

      <div class="actions">
        <button class="btn btn--cancel" @click="close()">Cancel</button>
        <button class="btn btn--confirm" @click="confirm(true)">OK</button>
      </div>
    </ModalContent>
  </ModalRoot>
</template>

<style scoped>
/* Center the card; padding keeps it from touching screen edges. */
.root {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1rem;
}

.card {
  width: 100%;
  max-width: 24rem;
  padding: 1.5rem;
  border-radius: 0.5rem;
  background: white;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.card[data-state='open'] {
  animation: card-slide-up 250ms cubic-bezier(0.16, 1, 0.3, 1);
}
.card[data-state='closed'] {
  animation: card-slide-down 200ms cubic-bezier(0.4, 0, 1, 1) forwards;
}
@keyframes card-slide-up {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes card-slide-down {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(24px); }
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
}
.btn {
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background-color 150ms ease;
}
.btn--cancel {
  background: transparent;
  color: #525252;
  border-color: #d4d4d4;
}
.btn--cancel:hover { background: #f5f5f5; }
.btn--confirm {
  background: #2563eb;
  color: white;
}
.btn--confirm:hover { background: #1d4ed8; }
</style>

<ModalRoot> must contain a <ModalContent> — without it the exit animation never finalizes and the modal hangs in the DOM.

defineOptions({ modalGroup: 'default' }) binds this component to the default group, so openModal(ConfirmDialog) works without passing group. Drop it if you'd rather pass group on every call.

Opening a modal

openModal returns a promise. confirm(data) resolves it with data; close() rejects it with a ModalClosedError. The simplest pattern catches the rejection and treats it as a falsy result:

DeleteButton.vue
import { openModal } from '@kolirt/vue-modal'
import ConfirmDialog from './ConfirmDialog.vue'

const ok = await openModal<boolean>(ConfirmDialog, {
  props: { message: 'Delete this project?' }
}).catch(() => false)

if (ok) {
  // user pressed OK
}

Press OKok is true. Press Cancel, Esc, or click outside → the promise rejects, .catch swallows it, and ok is false.

Try it

Live demo
Copyright © 2026