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

@j1nn0/vue-modal-dialog

v0.13.1

Published

A reusable Vue 3 modal dialog component with focus trap and ARIA support

Readme

vue-modal-dialog

A reusable Vue 3 modal dialog component with focus trap and ARIA accessibility support.

📦 Project Info

License npm version Downloads

⚙️ Build & Quality

Bundle Size Vite

🛠 Tech Stack

Vue TypeScript ESLint Oxlint Oxfmt Vitest


📑 Table of Contents


✨ Features

  • Vue 3 support
  • Focus trap inside the modal
  • Focus restoration: the element that opened the dialog is re-focused when the last dialog closes
  • Keyboard accessibility (Escape to close)
  • Backdrop with blur and fade animation
  • Supports multiple modals opened simultaneously with automatic stack management
  • Header, body, and footer slots
  • Optional footer slot
  • Close button in the header
  • Configurable dialog size: sm, md, lg, fullscreen
  • Configurable dialog width (supports custom widths via width prop for flexible layouts)
  • Supports dark mode and light mode via the mode prop ("light", "dark", or null to follow OS/browser preference)
  • New v0.12.0 Features:
    • Teleport support: render dialog anywhere in the DOM (e.g., to body)
    • Non-modal support: allow background interaction
    • Draggable dialogs: reposition dialog by dragging the header
    • Programmatic API: control dialog state via useDialog() composable
    • Before-close guard: prevent closing based on logic (e.g., unsaved changes)
    • Custom transitions: configurable entry/exit animations for dialog and backdrop
    • Initial focus: explicitly define which element to focus on open
    • Role configuration: choose between dialog or alertdialog
    • Body scroll locking: automatically prevent background scrolling
    • Expanded positioning: 9-way positioning system (center, top, bottom, corners, etc.)
    • New lifecycle events: before-open, opening, before-close, closing
    • Programmatic close: requestClose() method exposed to parents

🧪 Storybook

Use Storybook to interactively verify modal behavior and props.

pnpm storybook

Build static Storybook output:

pnpm build-storybook

💾 Installation

npm install @j1nn0/vue-modal-dialog

or

yarn add @j1nn0/vue-modal-dialog

⚙️ Peer Dependencies

Before using this component, make sure you have installed the following peer dependencies:

npm install vue @vueuse/core @vueuse/integrations focus-trap

or

yarn add vue @vueuse/core @vueuse/integrations focus-trap

These dependencies are required for the library to function properly.


🛠 Usage

You can use this component in two ways:

  1. Import individually (recommended, enables tree-shaking)
  2. Register globally as a Vue plugin

1️⃣ Individual Import (recommended)

<script setup>
import { ref } from 'vue';
import { VueModalDialog } from '@j1nn0/vue-modal-dialog';
import '@j1nn0/vue-modal-dialog/dist/vue-modal-dialog.css';

const isOpen = ref(false);

const submitForm = () => {
  alert('Form submitted!');
  isOpen.value = false;
};
</script>

<template>
  <button @click="isOpen = true">Open Dialog</button>

  <VueModalDialog v-model="isOpen">
    <!-- Header slot -->
    <template #header> Dialog Title </template>

    <!-- Body slot (default) -->
    <p>
      This is the body content of the dialog. It supports long text and will wrap automatically.
    </p>

    <!-- Footer slot (optional) -->
    <template #footer>
      <button @click="isOpen = false">Cancel</button>
      <button @click="submitForm">Submit</button>
    </template>
  </VueModalDialog>
</template>

Multiple Modals (Stack)

You can open multiple dialogs at the same time. Stack behavior is handled automatically.

<script setup>
import { ref } from 'vue';
import { VueModalDialog } from '@j1nn0/vue-modal-dialog';

const showDialog1 = ref(false);
const showDialog2 = ref(false);
</script>

<template>
  <button @click="showDialog1 = true">Open Dialog 1</button>

  <VueModalDialog v-model="showDialog1">
    <template #header>Dialog 1</template>
    <p>First dialog</p>
    <template #footer>
      <button @click="showDialog2 = true">Open Dialog 2</button>
    </template>
  </VueModalDialog>

  <VueModalDialog v-model="showDialog2">
    <template #header>Dialog 2</template>
    <p>Second dialog (topmost while open)</p>
  </VueModalDialog>
</template>

When multiple dialogs are open, only the topmost dialog handles Escape and backdrop close.


2️⃣ Global Plugin Registration

// main.js
import { createApp } from 'vue';
import App from './App.vue';

import { VueModalDialogPlugin } from '@j1nn0/vue-modal-dialog';
import '@j1nn0/vue-modal-dialog/dist/vue-modal-dialog.css';

const app = createApp(App);

// Registers globally as <VueModalDialog> by default
app.use(VueModalDialogPlugin);
// Or specify a custom name
// app.use(VueModalDialogPlugin, { name: 'CustomName' });

app.mount('#app');

Use <VueModalDialog> (or your custom name) anywhere in your app without importing it:

<template>
  <VueModalDialog v-model="isOpen">
    <template #header> Global Dialog </template>
    <p>Body content</p>
  </VueModalDialog>
</template>

🌐 CDN Usage

You can use @j1nn0/vue-modal-dialog via CDN without any bundler. Both individual import and global plugin usage are supported.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Modal Dialog CDN Example</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/tabbable/dist/index.umd.js"></script>
    <script src="https://unpkg.com/focus-trap/dist/focus-trap.umd.js"></script>
    <script src="https://unpkg.com/@vueuse/shared"></script>
    <script src="https://unpkg.com/@vueuse/core"></script>
    <script src="https://unpkg.com/@vueuse/integrations"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/@j1nn0/vue-modal-dialog/dist/vue-modal-dialog.css"
    />
    <script src="https://unpkg.com/@j1nn0/vue-modal-dialog/dist/vue-modal-dialog.umd.js"></script>
  </head>
  <body>
    <div id="app">
      <!-- Individual Import -->
      <button type="button" @click="isOpenImport = true">Open Import Dialog</button>
      <vue-modal-dialog v-model="isOpenImport">
        <template #header>Import Dialog Title</template>
        <p>Body content goes here</p>
        <template #footer>
          <button @click="isOpenImport = false">Close</button>
        </template>
      </vue-modal-dialog>

      <!-- Global Plugin -->
      <button type="button" @click="isOpenGlobal = true">Open Global Dialog</button>
      <global-plugin-modal-dialog v-model="isOpenGlobal">
        <template #header>Global Dialog Title</template>
        <p>Body content goes here</p>
        <template #footer>
          <button @click="isOpenGlobal = false">Close</button>
        </template>
      </global-plugin-modal-dialog>
    </div>

    <script>
      const { createApp, ref } = Vue;
      const { VueModalDialogPlugin, VueModalDialog } = J1nn0VueModalDialog;

      const app = createApp({
        setup() {
          const isOpenImport = ref(false);
          const isOpenGlobal = ref(false);

          return { isOpenImport, isOpenGlobal };
        },
      });

      // Individual import registration
      app.component('VueModalDialog', VueModalDialog);

      // Global plugin registration (default name: 'VueModalDialog')
      app.use(VueModalDialogPlugin, { name: 'GlobalPluginModalDialog' });

      app.mount('#app');
    </script>
  </body>
</html>

📌 Props

| Prop | Type | Default | Description | | -------------------- | ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | backdrop | Boolean | String | true | true = backdrop click closes dialog, false = no backdrop, "static" = backdrop shown but click does not close | | escape | Boolean | true | Pressing Escape key closes the dialog | | role | String | "dialog" | ARIA role: "dialog" or "alertdialog" | | initialFocus | String | HTMLElement | undefined | Element selector or element to focus when the dialog opens | | modal | Boolean | true | true = blocks background interaction and traps focus | | teleport | Boolean | String | false | true = teleports to body, or specify a CSS selector target | | scrollLock | Boolean | true | Locks page scrolling while the dialog is open | | draggable | Boolean | false | Enables dragging the dialog by its header | | transition | String | "fade" | Transition name for the dialog panel | | backdropTransition | String | "fade-backdrop" | Transition name for the backdrop layer | | beforeClose | Function | undefined | Async or sync callback; return false to prevent closing | | position | String | "center" | Position: "center", "top", "bottom", "left", "right", "topleft", "topright", "bottomleft", "bottomright" | | width | String | "md" | Dialog width. Presets: sm, md, lg, fullscreen. Also supports custom CSS width, e.g. "400px", "50%", "80vw" | | mode | String | null | null | Dialog color mode: "light" for light mode, "dark" for dark mode, null to follow the OS/browser preference |


🎛 Slots

| Slot | Description | | -------- | --------------------------------------------------------- | | header | Optional. Content for the header. × button always present | | default | Content for the body of the dialog | | footer | Optional. Content for footer, not rendered if empty |


🔔 Events

| Event | Payload | Description | | -------------- | ------- | --------------------------------------------------- | | before-open | void | Fired before the dialog begins its opening sequence | | opening | void | Fired when the opening transition starts | | opened | void | Fired when the opening transition completes | | before-close | void | Fired before the dialog begins its closing sequence | | closing | void | Fired when the closing transition starts | | closed | void | Fired when the closing transition completes |


🔓 Expose

| Method | Description | | -------------- | --------------------------------------------------------------------------- | | requestClose | Programmatically request to close the dialog. Respects beforeClose guard. |


♿ Accessibility

  • role="dialog" + aria-modal="true" on the topmost dialog
  • aria-modal="false" + aria-hidden="true" on lower-layered dialogs
  • aria-labelledby points to header slot
  • aria-describedby points to body slot
  • Close button has aria-label="Close"
  • Focus trap is active on the topmost dialog to keep keyboard navigation predictable
  • Focus is restored to the element that was focused before the first dialog opened when the last dialog closes
  • Escape key closes the dialog if enabled (topmost dialog only when stacked)

🎨 Styles

  • Dialog width: sm, md, lg, fullscreen
  • Dialog height: auto, max 80vh (default), scrollable if content overflows
  • Word wrapping enabled in header and body
  • Backdrop has fade-in/out animation with blur effect
  • Supports Light and Dark mode via mode prop

CSS Custom Properties

:root {
  /* Backdrop */
  --j1nn0-vue-modal-dialog-backdrop-z-index: 1000;
  --j1nn0-vue-modal-dialog-backdrop-background: rgba(0, 0, 0, 0.6);
  --j1nn0-vue-modal-dialog-backdrop-blur: 2px;

  /* Dialog */
  --j1nn0-vue-modal-dialog-border: none;
  --j1nn0-vue-modal-dialog-border-radius: 8px;
  --j1nn0-vue-modal-dialog-width: 90%;
  --j1nn0-vue-modal-dialog-max-width-sm: 300px;
  --j1nn0-vue-modal-dialog-max-width-md: 600px;
  --j1nn0-vue-modal-dialog-max-width-lg: 900px;
  --j1nn0-vue-modal-dialog-max-height: 80vh;
  --j1nn0-vue-modal-dialog-text-color: #000000;

  /* Header */
  --j1nn0-vue-modal-dialog-header-background: #f5f5f5;
  --j1nn0-vue-modal-dialog-header-padding: 1rem;

  /* Body */
  --j1nn0-vue-modal-dialog-body-background: #fff;
  --j1nn0-vue-modal-dialog-body-padding: 1rem;

  /* Footer */
  --j1nn0-vue-modal-dialog-footer-background: #f5f5f5;
  --j1nn0-vue-modal-dialog-footer-padding: 1rem;

  /* Dark mode */
  --j1nn0-vue-modal-dialog-backdrop-background-dark: rgba(255, 255, 255, 0.2);
  --j1nn0-vue-modal-dialog-border-dark: none;
  --j1nn0-vue-modal-dialog-header-background-dark: #1f2937;
  --j1nn0-vue-modal-dialog-footer-background-dark: #1f2937;
  --j1nn0-vue-modal-dialog-body-background-dark: #111827;
  --j1nn0-vue-modal-dialog-text-color-dark: #f9fafb;
}

📝 Notes on Multiple Modals

This library supports multiple modals opened simultaneously.

When dialogs are stacked:

  • Only the topmost dialog responds to Escape and backdrop click
  • Only the topmost dialog renders a backdrop (fullscreen dialogs do not render a backdrop)
  • Focus trap is active only for the topmost dialog
  • ARIA attributes are updated by stack position:
    • topmost dialog: aria-modal="true", aria-hidden="false"
    • lower-layered dialogs: aria-modal="false", aria-hidden="true"
  • Dialog z-index is automatically calculated from stack order
  • Focus is restored to the element that triggered the first dialog when all dialogs are closed

No additional configuration is required to use stack behavior.


🎯 Programmatic API

You can use the useDialog composable to control the dialog state from any child component or logic.

<script setup>
import { VueModalDialog, useDialog } from '@j1nn0/vue-modal-dialog';

const { isOpen, open, close } = useDialog();
</script>

<template>
  <button @click="open">Open via API</button>

  <VueModalDialog v-model="isOpen">
    <p>Controlled via useDialog()</p>
    <button @click="close">Close</button>
  </VueModalDialog>
</template>

🖱 Draggable Dialogs

Enable header-based dragging by adding the draggable prop.

<VueModalDialog v-model="isOpen" draggable>
  <template #header>Drag Me</template>
  <p>You can move this dialog anywhere on the screen.</p>
</VueModalDialog>

🚪 Non-modal Dialogs

Set modal to false to allow interaction with the background while the dialog is open.

<VueModalDialog v-model="isOpen" :modal="false" backdrop="static">
  <p>The background remains interactive.</p>
</VueModalDialog>

🔒 Prevent Close

Use beforeClose to add validation or confirmation before the dialog closes.

<script setup>
const handleBeforeClose = async () => {
  return window.confirm('You have unsaved changes. Close anyway?');
};
</script>

<template>
  <VueModalDialog v-model="isOpen" :beforeClose="handleBeforeClose">
    <p>Try to close me.</p>
  </VueModalDialog>
</template>

🏷 License

MIT License

Copyright © 2025–PRESENT j1nn0