vue-modal
๐งฉ Call modals imperatively, declare them declaratively โ a minimal, type-safe modal framework for Vue 3.
vue-modal lets you register a modal once and then drive it from anywhere โ open it, close it, hand it fully-typed props โ without prop-drilling a single isOpen flag through your component tree. It's the elevator, not the staircase: you don't pour a new shaft on every floor, you mount one collector and press the button (modal.open()) from wherever you happen to be standing.
๐ค Features
Type-safe by construction โ
useModalreads your component's own props and makes you pass the required ones; the wrong shape gets caught by your editor, not by your users.Open and close from anywhere โ modal state lives outside the component tree, so whoever holds the composable holds the off-switch. No
isOpenref drilled through five components that don't care.Teleport-rendered โ mount one
<ModalCollector />and every modal renders at<body>with a z-index that always wins; nooverflow: hiddenparent clips it in half.Esc and click-outside โ both dismissals wired out of the box, the two things every human reflexively reaches for.
Zero CSS to import โ built-in fade (tune it with
transitionDuration), styles injected straight from the JS; no stylesheet for you to forget.
And the bits you'd expect to just work:
one modal on screen at a time (why would you ever want two? ๐คจ)
a
ModalCollectorslot for bringing your own overlay and wrapper<script setup>-native, Composition API throughoutan
unpluginresolver + preset, souseModalandModalCollectorauto-import themselves
Table of Contents
โธป
๐ฆ Installation
npm install @dsplce-co/vue-modal
# or
yarn add @dsplce-co/vue-modal
# or
pnpm add @dsplce-co/vue-modalThat's the whole dependency โ it leans on Vue 3 and nothing else you have to install yourself (see Requirements).
โธป
๐งช Usage
Four steps: install the plugin, drop the collector in once, write a modal, open it. The first two you do a single time; after that it's just useModal wherever you need it.
1. Install the plugin
The plugin wires up the global modal state every useModal call reaches into. Add it in your app entry:
import { createApp } from 'vue';
import VueModalPlugin from '@dsplce-co/vue-modal';
import App from './App.vue';
const app = createApp(App);
app.use(VueModalPlugin);
app.mount('#app');2. Drop in the collector
ModalCollector is the single place all your modals actually render โ your one elevator shaft. Put it once at your app root and never think about it again:
<template>
<div id="app">
<router-view />
<users-view /> <!-- We'll get to this in a moment -->
<modal-collector />
</div>
</template>
<script setup>
import { ModalCollector } from '@dsplce-co/vue-modal';
</script>3. Build a modal
Here's the part people expect to be hard and isn't: a modal is just a component. Say you've got a user list and you want an "are you sure?" before deleting one โ that confirmation dialog is a plain Vue component that takes props like any other and emits close when it's done:
<template>
<div class="confirmation-modal">
<h2>Confirm Action</h2>
<p>Are you sure you want to delete {{ user.name }}?</p>
<div class="confirmation-modal__actions">
<button @click="$emit('close')">Cancel</button>
<button @click="confirmDelete">Confirm</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { User } from './UsersView.vue';
const props = defineProps({
user: {
type: Object as () => User,
required: true,
},
onConfirm: {
type: Function,
required: true,
},
});
const emit = defineEmits(['close']);
const confirmDelete = () => {
props.onConfirm(props.user.id);
emit('close');
};
</script>
<style>
.confirmation-modal {
background: white;
padding: 2rem;
max-width: 400px;
width: 100%;
}
.confirmation-modal__actions {
display: flex;
gap: .5rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
.confirmation-modal__actions button {
padding: 0.5rem 1rem;
border: none;
cursor: pointer;
}
.confirmation-modal__actions button:first-child {
background: #e5e7eb;
}
.confirmation-modal__actions button:last-child {
background: #ff3b89;
color: white;
}
</style>4. Open it with useModal
Now wire it up. useModal takes your component and hands back an open/close pair โ and because it read your props, it knows user and onConfirm are required and won't let you call open() without them:
<template>
<div class="users-view">
<!-- โ Notice the ConfirmationModal is not mounted directly -->
<!-- anywhere โ it is the ModalCollector's job to render modals -->
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
<button @click="onDelete(user)">Delete</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { useModal } from '@dsplce-co/vue-modal';
import { ref } from 'vue';
import ConfirmationModal from './ConfirmationModal.vue';
export type User = {
id: string;
name: string;
};
const users = ref<User[]>([
{ id: '1', name: 'Walter White' },
{ id: '2', name: 'Hank Schrader' },
]);
const deleteUser = (id) => {
console.log('Deleting user with id:', id);
// Your deletion logic here
};
// Register the modal
const modal = useModal(ConfirmationModal);
const onDelete = (user: User) => {
// Open modal with required props
modal.open({
user,
onConfirm: () => deleteUser(user.id),
});
};
</script>โธป
๐ API reference
VueModalPlugin
The Vue plugin that sets up global modal state. Install it once โ everything else assumes it's there, and useModal throws a clear error if it isn't:
import { createApp } from 'vue';
import VueModalPlugin from '@dsplce-co/vue-modal';
app.use(VueModalPlugin);ModalCollector
The component that renders the active modal, via Vue's <teleport>. Mount it once, near your app root.
| Prop | Type | Default | What it does |
|---|---|---|---|
transitionDuration | string | "0.5s" | Length of the fade in/out โ any CSS time value ("200ms", "1s", โฆ) |
It also exposes a default slot โ { component, payload, close } โ for when you want to render the overlay and wrapper yourself. See Custom modal overlay and wrapper.
<template>
<ModalCollector />
<!-- or, slower fade: -->
<ModalCollector transition-duration="1s" />
</template>useModal
Creates a typed controller for one modal component:
import { useModal } from '@dsplce-co/vue-modal';
const modal = useModal(YourModalComponent);Returns:
open(props)โ opens the modal, passingpropsthrough to your componentclose()โ closes whatever's open
Type safety is the whole point. The controller infers from your component whether props are required or optional, and the compiler holds you to it:
// If the modal has required props
modal.open({ requiredProp: 'value' }); // โ
TypeScript enforces this
// If the modal has only optional props
modal.open(); // โ
props can be omitted
modal.open({ optionalProp: 'value' }); // โ
or providedModalOverlay
The default backdrop โ a fixed, centered, blurred overlay that the collector wraps your modal in automatically. You only import it directly when you're building a custom collector slot and want the stock backdrop back. Like ModalCollector, it's exported from the package and resolvable via the unplugin resolver.
What the collector handles
Teleport โ modals render at
<body>level, above everything (z-index: 10000)Backdrop โ a semi-transparent overlay with a
blur(4px)behind your contentDismissal โ
Escand click-outside both close the active modalTransitions โ fade in/out, timed by
transitionDurationOne at a time โ opening a modal replaces whatever was already open
Centering โ full-viewport overlay, content centered
On accessibility: vue-modal deliberately stays out of your modal's content, which means it doesn't impose ARIA roles or a focus trap on you.
role="dialog",aria-modal, labelling and focus management belong in your own modal component โ that's where the content lives, so that's where they make sense. Wire them there.
Writing a modal component
Whatever you open just needs to play along with two things:
Emit
closeโ$emit('close')(ordefineEmits(['close'])) is how your modal asks to be dismissed. The collector also closes onEscand click-outside, but an in-modal "Close" button needs this.Declare props normally โ
definePropsas usual; the collector binds whatever you passed toopen()straight onto your component.
Styling the content is yours; the overlay and positioning are the collector's (see Styling).
<template>
<div class="my-modal">
<h2>{{ title }}</h2>
<button @click="$emit('close')">Close</button>
</div>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
required: true,
},
});
defineEmits(['close']);
</script>โธป
๐จ Styling
The library brings the bare minimum: the overlay, the blur, the centering, the fade. Everything inside the modal โ the card, the padding, the buttons โ is yours, because it's your component. No design opinions shipped, nothing of yours to override.
โธป
๐ง Advanced usage
Auto-import with unplugin
vue-modal ships a resolver for unplugin-vue-components and a preset for unplugin-auto-import, so the components and the composable show up without you importing them by hand.
npm install -D unplugin-vue-components unplugin-auto-import// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { VueModalResolver, VueModalPreset } from '@dsplce-co/vue-modal/resolver'
export default defineConfig({
plugins: [
Components({ resolvers: [VueModalResolver()] }),
AutoImport({ imports: [VueModalPreset] }),
],
})Now ModalCollector and ModalOverlay are auto-imported as components, and useModal is globally available without imports:
<script setup lang="ts">
import ConfirmationModal from './ConfirmationModal.vue'
const modal = useModal(ConfirmationModal) // no import needed
</script>
<template>
<ModalCollector /> <!-- no import needed -->
</template>Custom modal overlay and wrapper
Don't like the stock overlay? The ModalCollector default slot hands you the active component, its payload, and a close function โ render the lot however you like:
<ModalCollector v-slot="{ component, payload, close }">
<div v-if="component !== null" class="custom-overlay">
<div class="custom-modal-container">
<div class="modal-header">
<button @click="close">ร</button>
</div>
<component :is="component" v-bind="payload" @close="close" />
</div>
</div>
</ModalCollector>โธป
๐ ๏ธ Requirements
Vue 3 โ declared as a peer dependency (
3.x)
โธป
๐ Repo & Contributions
๐ฆ Package: @dsplce-co/vue-modal ๐ ๏ธ Repo: github.com/dsplce-co/vue-modal
Contributions, issues, suggestions welcome. Hit us up ๐ค
โธป
๐ License
MIT or Apache-2.0, at your option.