@dsplce-co/vue-modal
v0.4.3
Published
Utility library for displaying modals in Vue
Readme
We're dsplce.co, check out our work on our website: dsplce.co 🖤
vue-modal
🧩 Modal composable for Vue — A minimal and type-safe framework for modals in Vue.js applications.
🖤 Features
✅ Type-safe modal system with generics ✅ Automatic prop requirement inference ✅ Close your modals from anywhere with composables ✅ ARIA-compliant ✅ Esc key handling ✅ Click outside to close ✅ Teleport-based rendering with proper z-index ✅ Built-in smooth transitions ✅ Zero external CSS dependencies ✅ Vue 3 Composition API ready
📦 Installation
Add to your package.json:
npm install @dsplce-co/vue-modal
# or
yarn add @dsplce-co/vue-modal
# or
pnpm add @dsplce-co/vue-modalThis package requires Vue 3.
🧪 Usage
1. Set up the plugin
Install the Vue Modal plugin in your main application file to enable global modal state management:
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. Add modal collector
Add the ModalCollector component to your app root to enable modal rendering. This will manage the rendering of all modals in a single location.
<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. Create modal component
Imagine in your application there is a user list view, and you want to add the functionality to delete a user. You decide a confirmation dialog would come in handy.
In vue-modal, your modal component can be any regular Vue component. It receives props as usual and can emit a close event:
<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. Use the modal
Now that you've defined the confirmation modal, let's use it with the useModal composable:
<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
Plugin setup
VueModalPlugin
Vue plugin that sets up global modal state management.
import { createApp } from 'vue';
import VueModalPlugin from '@dsplce-co/vue-modal';
app.use(VueModalPlugin);Components
ModalCollector
Component that manages modal rendering using Vue's teleport system.
<template>
<ModalCollector />
</template>Composables
useModal
Creates a typed modal controller for a specific component:
import { useModal } from '@dsplce-co/vue-modal';
const modal = useModal(YourModalComponent);Returns an object with:
open(props)- Opens the modal with provided propsclose()- Closes the modal
Type safety: The composable automatically infers whether props are required or optional based on your component's prop definitions:
// If modal has required props
modal.open({ requiredProp: 'value' }); // ✅ TypeScript enforces this
// If modal has only optional props
modal.open(); // ✅ Props can be omitted
modal.open({ optionalProp: 'value' }); // ✅ Or providedModal component guidelines
Your modal components should:
- Emit
closeevent: Use$emit('close')ordefineEmits(['close'])to enable closing - Handle props: Define props normally using
definePropsorpropsoption - Style appropriately: Apply styles for the modal content (overlay is handled by the collector)
<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>Modal features
- Accessibility: Proper ARIA attributes and focus management
- Keyboard Navigation: Esc key closes modal automatically
- Click Outside: Click outside the modal content to close
- Portal Rendering: Modals render at the body level using Vue's teleport
- Single Modal: Only one modal can be open at a time (why would you want to show more than one modal at a time? 🤨)
- Transitions: Smooth fade in/out animations
- Responsive: Full viewport coverage with centered content
- Backdrop: Semi-transparent backdrop with blur effect
🎨 Styling
The library provides minimal base styles for the overlay and positioning. You're responsible for styling your modal components.
🔧 Advanced usage
Custom modal overlay and wrapper
You can customise how modals are rendered by using the ModalCollector's slot:
<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>📁 Repo & contributions
📦 Package: @dsplce-co/vue-modal 🛠️ Repo: github.com/dsplce-co/vue-modal
Contributions, issues, ideas? Hit us up 🖤
🔒 License
MIT or Apache-2.0, at your option.
