npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noeldemartin/vue-modals

v0.1.1

Published

The missing library to do Vue Modals right:

Readme

Vue Modals CI

The missing library to do Vue Modals right:

  • 🪐 Open modals from anywhere (even outside of Vue components!).
  • 📤 Return promises from modals for seamless async workflows.
  • 🧼 Clean and simple API.
  • 🧙 Full TypeScript support.
  • 📦 Tiny library with zero dependencies.

To learn more, read this blog post: The Problems With Modals, and How to Solve Them.

Installation

First, install it using your favorite package manager:

pnpm add @noeldemartin/vue-modals

Then, place a <ModalsPortal> somewhere within your app (wherever you want to render modals).

Finally, open modals like this:

import { showModal } from '@noeldemartin/vue-modals';
import MyModal from './MyModal.vue';

const { answer } = await showModal(MyModal, { question: 'How many golf balls fit into a Boeing 747?' });

Usage

[!NOTE] You can take this for a spin in the vue-modals-sandbox, a bare bones Vue application with Shadcn and this library.

The second argument in the showModal function will be passed as component properties, and modals can be closed by emitting a close event (the payload of which will be returned in a promise). You can take advantage of Vue's TypeScript features to type both:

<template>
    <Modal>
        <button @click="$emit('close', { answer: 'The Answer' })">Close</button>
    </Modal>
</template>

<script setup lang="ts">
defineEmits<{ close: [{ answer: string }] }>();
defineProps<{ question: string }>();
</script>
const { answer } = await showModal(MyModal, { question: 'How many golf balls fit into a Boeing 747?' });
// 👆 answer will be typed as `string | undefined` (in case the modal is dismissed)
// 👆 showModal's second argument will be typed as `{ question: string }`

Modal responses

Given that modals can also be dismissed, the payload from the close event won't be directly returned in the promise. Instead, it will be merged into an object with a dismissed boolean:

defineEmits<{ close: [{ answer: string }] }>();
// 👆 showModal will return `Promise<{ dismissed: false; answer: string } | { dismissed: true; answer?: undefined }>`

defineEmits<{ close: [string] }>();
// 👆 showModal will return `Promise<{ dismissed: false; response: string } | { dismissed: true; response?: undefined }>`

defineEmits<{ somethingElse: [] }>();
// 👆 showModal will return `Promise<{ dismissed: boolean }>`

Customizing modals

If you want to have an overlay that sits behind your modals, you can use the overlay slot in the <ModalsPortal>. You can also style the modals container passing class or style attributes, and use <Transition> to implement animations:

<ModalsPortal class="fixed inset-0 flex items-center justify-center pointer-events-none">
    <template #overlay="{ show }">
        <Transition
            enter-active-class="transition-opacity duration-300"
            enter-from-class="opacity-0"
            enter-to-class="opacity-100"
            leave-active-class="transition-opacity duration-300"
            leave-from-class="opacity-100"
            leave-to-class="opacity-0"
        >
            <div v-if="show" class="fixed inset-0 bg-gray-500/75" />
        </Transition>
    </template>
</ModalsPortal>

By default, modals will be rendered one after another in the DOM. However, some libraries may require to nest them (such as Reka UI). In that case, you can use the nested attribute to indicate that the rendering of child modals will happen in each modal component, rather than the root:

<!-- This will only render the first active modal -->
<ModalsPortal nested />

Modals don't need to use any special components, so they can be simple divs:

<div class="bg-white p-4 rounded-lg shadow-lg z-10 pointer-events-auto">
    <p>My modal content</p>
</div>

However, if you also want to configure some animations, you can use the <Modal> component and pass the same attributes used in the <Transition> component:

<Modal
    enter-active-class="transition-all duration-300"
    enter-from-class="opacity-0 scale-95"
    enter-to-class="opacity-100 scale-100"
    leave-active-class="transition-all duration-300"
    leave-from-class="opacity-100 scale-100"
    leave-to-class="opacity-0 scale-95"
    class="bg-white p-4 rounded-lg shadow-lg z-10 pointer-events-auto"
>
    <p>My modal content</p>
</Modal>

Finally, if you want to create your own modal wrapper, you can use useModal(). This will expose a couple of utilities you can use to work with the modal. By default, modals are removed from the DOM when closed, but you can pass the { removeOnClose: false } option to disable it. If you do, make sure to call remove() to remove it yourself. Combined with visible, and using the native <Transition>, you can achieve the same result as the <Modal> component to customize it on your own:

<template>
    <Transition @after-leave="remove()">
        <div v-if="visible">
            <slot :close />
        </div>
    </Transition>
</template>

<script setup lang="ts">
import { useModal } from '@noeldemartin/vue-modals';

const { visible, close, remove } = useModal({ removeOnClose: false });
</script>

You can also use child if you want to render nested modals using the <ModalComponent>:

<template>
    <Transition @after-leave="remove()">
        <div v-if="visible">
            <slot :close />
        </div>
    </Transition>

    <ModalComponent v-if="child" :is="child" />
</template>

<script setup lang="ts">
import { useModal } from '@noeldemartin/vue-modals';

const { child, visible, close, remove } = useModal({ removeOnClose: false });
</script>

However, keep in mind that if you're going to work with modals this way, you'll need to implement all the accessibility functionality on your own (focus trapping, keyboard events, etc.).

Instead, you'll probably want to integrate with an existing component library.

Third-party integrations

In order to use this with component libraries, you'll need to follow similar techniques from the ones described in the previous section.

There are some built-in integrations, but feel free to look at the src/integrations/ folder to learn more.

PrimeVue

You can use the <ModalsPortal> component as usual, but import <Modal> from @noeldemartin/vue-modals/primevue instead. Attributes will be passed down to PrimeVue's native <Dialog> component, such as header, footer, maximizable, etc.

<template>
    <Modal header="My Awesome Modal">
        <p>My modal content</p>
    </Modal>
</template>

<script setup lang="ts">
import Button from 'primevue/button';
import { Modal } from '@noeldemartin/vue-modals/primevue';
</script>

Shadcn

Following Shadcn's philosophy, this library doesn't export any code to integrate with the library. Instead, you'll want to copy & paste these into your project:

src/components/ui/modal/Modal.vue

<template>
    <Dialog :open="visible" @update:open="$event || close()">
        <DialogContent>
            <slot />

            <ModalComponent v-if="child" :is="child" />
        </DialogContent>
    </Dialog>
</template>

<script setup lang="ts">
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { ModalComponent } from '@noeldemartin/vue-modals';
import { useModal } from './utils';

const { child, visible, close } = useModal();
</script>

src/components/ui/modal/ModalsPortal.vue

<template>
    <ModalsPortal nested />
</template>

<script setup lang="ts">
import { ModalsPortal } from '@noeldemartin/vue-modals';
</script>

src/components/ui/modal/utils.ts

import { useModal as _useModal } from '@noeldemartin/vue-modals';

export function useModal<T = never>() {
    const modal = _useModal<T>({ removeOnClose: false });

    return {
        ...modal,
        close(payload?: T) {
            modal.close(payload);

            setTimeout(() => modal.remove(), 300);
        },
    };
}

src/components/ui/modal/index.ts

export * from './utils';
export { default as Modal } from './Modal.vue';
export { default as ModalsPortal } from './ModalsPortal.vue';

Once that is set up, you should be able to create modals like this:

<template>
    <Modal>
        <DialogHeader>
            <DialogTitle>My Awesome Modal</DialogTitle>
        </DialogHeader>
        <p>Modal content goes here</p>
    </Modal>
</template>

<script setup lang="ts">
import { Modal } from '@/components/ui/modal';
import { DialogHeader, DialogTitle } from '@/components/ui/dialog';
</script>