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 OK → ok is true. Press Cancel, Esc, or click outside → the promise rejects, .catch swallows it, and ok is false.
Try it
Live demo
