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

overlay-manager-vue

v1.0.0

Published

Declarative overlay manager for Vue 3 with hook-based API - Promise-based modal, dialog, toast, and drawer control with full TypeScript support, multiple stacking, and lifecycle management

Readme

overlay-manager-vue

npm version npm downloads license bundle size

A Vue 3 overlay management library with hook-based API, inspired by overlay-rc.

Version 1.0.0 - Production-ready with full reactivity and multiple overlay stacking support.

Korean Version (한글 버전) | Migration Guide | API Reference

Features

  • Hook-based API - useOverlay(), useCurrentOverlay(), useOverlayController()
  • Fully Reactive - All context properties are reactive with Vue's computed
  • Type-safe - Full TypeScript support with generics
  • Promise-based - Async/await overlay control with result handling
  • Multiple Stacking - Multiple overlays can be open simultaneously
  • Lifecycle callbacks - onMounted, onUnmounted support
  • Framework agnostic - Works with Radix Vue, reka-ui, Headless UI, or custom components
  • Comprehensive Tests - 97%+ test coverage with Vitest

Bundle Size

Lightweight and optimized for production:

| Format | Raw Size | Gzipped (Actual Download) | |--------|----------|---------------------------| | ESM | 8.4 KB | 3.17 KB ⚡ | | CJS | 3.8 KB | 1.67 KB ⚡ |

  • Modern apps (ESM): Only 3.17 KB gzipped
  • Legacy support (CJS): Only 1.67 KB gzipped
  • Type definitions: 25.3 KB (development only)

Installation

npm install overlay-manager-vue
# or
yarn add overlay-manager-vue
# or
pnpm add overlay-manager-vue

Required Peer Dependencies

For overlay stacking support, you need to use Dialog components (not AlertDialog):

# Install reka-ui for Dialog components
pnpm add reka-ui @radix-icons/vue

# Or use shadcn-vue (recommended)
pnpm dlx shadcn-vue@latest add dialog

Important: AlertDialog from Radix Vue does NOT support stacking - only one can be open at a time. Use Dialog from reka-ui for multiple overlay support.

Quick Start

1. Setup OverlayHost

Add <OverlayHost /> to your root component (e.g., App.vue):

<script setup lang="ts">
import { OverlayHost } from 'overlay-manager-vue'
</script>

<template>
  <div id="app">
    <YourContent />

    <!-- Add OverlayHost once at the root -->
    <OverlayHost />
  </div>
</template>

2. Create an Overlay Component

Create a component using Dialog (supports stacking):

<!-- MyDialog.vue -->
<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{{ overlay.data.title }}</DialogTitle>
        <DialogDescription>
          {{ overlay.data.message }}
        </DialogDescription>
      </DialogHeader>
      <DialogFooter>
        <Button variant="outline" @click="handleCancel">Cancel</Button>
        <Button @click="handleConfirm">Confirm</Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

<script setup lang="ts">
import { useCurrentOverlay } from 'overlay-manager-vue'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'

interface DialogProps {
  title: string
  message: string
}

interface DialogResult {
  confirmed: boolean
}

// Define props to avoid Vue warning
defineProps<DialogProps>()

// Fully reactive context with TypeScript generics
const overlay = useCurrentOverlay<DialogProps, DialogResult>()

const handleConfirm = () => {
  overlay.close({ confirmed: true })
}

const handleCancel = () => {
  overlay.dismiss('user_cancelled')
}
</script>

Key Points:

  • ✅ Use Dialog from reka-ui (supports multiple instances)
  • ✅ Bind :open="overlay.isOpen" (fully reactive)
  • ✅ Use TypeScript generics for type safety
  • ✅ Define props with defineProps<T>()
  • ❌ Don't use AlertDialog (only allows one instance)
  • ❌ Don't use <Teleport> (handled by OverlayHost)

3. Open the Overlay

Use useOverlay() to open your overlay component:

<script setup lang="ts">
import { useOverlay } from 'overlay-manager-vue'
import MyDialog from './components/MyDialog.vue'

const { openOverlay } = useOverlay()

async function showDialog() {
  try {
    const result = await openOverlay(MyDialog, {
      data: {
        title: 'Confirm Action',
        message: 'Are you sure you want to proceed?',
      },
    })

    console.log('User confirmed:', result.data.confirmed)
  } catch (error) {
    console.log('User cancelled:', error.reason)
  }
}
</script>

<template>
  <button @click="showDialog">Open Dialog</button>
</template>

API Reference

useOverlay()

Hook for opening overlays from anywhere in your application.

const { openOverlay } = useOverlay()

const result = await openOverlay(Component, options)

Options

interface OverlayOptions<TData> {
  // Data to pass to the overlay component
  data?: Record<string, unknown>

  // Guard function called before closing
  beforeClose?: () => boolean | Promise<boolean>

  // Callback after component is mounted
  onMounted?: () => void

  // Callback after component is unmounted
  onUnmounted?: () => void
}

Return Value

Returns a Promise that resolves when the overlay is closed via close():

interface OverlayResult<T> {
  type: 'close' | 'dismiss'
  data?: T
}

Or rejects when dismissed via dismiss():

interface OverlayError {
  type: 'dismiss' | 'error'
  reason?: unknown
}

useCurrentOverlay()

Hook for accessing overlay context inside an overlay component.

const overlay = useCurrentOverlay<TData, TResult>()

overlay.id        // Unique overlay ID
overlay.isOpen    // Reactive open state (computed)
overlay.data      // Data passed to the overlay (reactive)
overlay.close(result)   // Close with result (resolves promise)
overlay.dismiss(reason) // Dismiss (rejects promise)

⚠️ Important:

  • Can only be used inside components rendered by OverlayHost
  • All properties are fully reactive (using Vue's computed)
  • Use TypeScript generics for type safety: useCurrentOverlay<TData, TResult>()

useOverlayController()

Hook for imperative overlay control with a controller object.

const controller = useOverlayController(Component, options)

controller.id      // Overlay ID
controller.result  // Promise<OverlayResult>
controller.close(result)   // Close the overlay
controller.dismiss(reason) // Dismiss the overlay

Example:

const controller = useOverlayController(MyDialog, {
  data: { title: 'Hello' }
})

// Close after 3 seconds
setTimeout(() => {
  controller.close({ confirmed: true })
}, 3000)

const result = await controller.result

<OverlayHost />

Component that renders all active overlays. Must be placed once in your app root.

<template>
  <div id="app">
    <YourContent />
    <OverlayHost />
  </div>
</template>

Advanced Examples

Multiple Stacked Overlays

Multiple overlays can be open at the same time (requires Dialog, not AlertDialog):

// Open first overlay
const promise1 = openOverlay(Dialog1, { data: { id: 1 } })

// Open second overlay (stacked on top)
const promise2 = openOverlay(Dialog2, { data: { id: 2 } })

// Open third overlay (stacked on top)
const promise3 = openOverlay(Dialog3, { data: { id: 3 } })

// All three overlays are visible and stacked
await Promise.all([promise1, promise2, promise3])

Sequential Overlay Chain

Open overlays in sequence, where each overlay can open the next one:

<script setup lang="ts">
import { useCurrentOverlay, useOverlay } from 'overlay-manager-vue'

interface SequentialData {
  step: number
  totalSteps: number
  message: string
}

defineProps<SequentialData>()

const overlay = useCurrentOverlay<SequentialData, string>()
const { openOverlay } = useOverlay()

const handleNext = async () => {
  if (overlay.data.step < overlay.data.totalSteps) {
    // Open next dialog (keeps current dialog open - stacked)
    await openOverlay(SequentialDialog, {
      data: {
        step: overlay.data.step + 1,
        totalSteps: overlay.data.totalSteps,
        message: `Step ${overlay.data.step + 1}`,
      },
    })
  }

  overlay.close('finished')
}
</script>

<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <DialogHeader>
        <DialogTitle>
          Dialog {{ overlay.data.step }} / {{ overlay.data.totalSteps }}
        </DialogTitle>
        <DialogDescription>
          {{ overlay.data.message }}
        </DialogDescription>
      </DialogHeader>
      <DialogFooter>
        <Button @click="handleNext">
          {{ overlay.data.step === overlay.data.totalSteps ? 'Finish' : 'Next' }}
        </Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

Reactive Context Example

All overlay context properties are fully reactive:

<script setup lang="ts">
import { watch } from 'vue'
import { useCurrentOverlay } from 'overlay-manager-vue'

interface CounterData {
  count: number
}

defineProps<CounterData>()

const overlay = useCurrentOverlay<CounterData, void>()

// Watch reactive properties
watch(() => overlay.isOpen, (isOpen) => {
  console.log('Overlay open state changed:', isOpen)
})

watch(() => overlay.data.count, (count) => {
  console.log('Count changed:', count)
})
</script>

<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <!-- Automatically re-renders when overlay.data changes -->
      <DialogTitle>Count: {{ overlay.data.count }}</DialogTitle>
    </DialogContent>
  </Dialog>
</template>

BeforeClose Guard

Prevent overlay from closing with a guard function:

const result = await openOverlay(MyDialog, {
  beforeClose: async () => {
    // Confirm with user before closing
    return window.confirm('Are you sure you want to close?')
  },
})

If beforeClose returns false, the overlay stays open. Use dismiss() to force close:

// Inside overlay component
overlay.dismiss('force_close') // Bypasses beforeClose guard

Lifecycle Callbacks

Execute code when overlay mounts/unmounts:

const result = await openOverlay(MyDialog, {
  onMounted: () => {
    console.log('Overlay mounted')
  },
  onUnmounted: () => {
    console.log('Overlay unmounted - cleanup here')
  },
})

Type-Safe Props and Results

Use generics for full type safety:

interface FormData {
  name: string
  email: string
}

interface FormResult {
  submitted: boolean
  data?: FormData
}

// In overlay component
const overlay = useCurrentOverlay<FormData, FormResult>()

// TypeScript knows overlay.data has name and email
const name = overlay.data.name

// TypeScript enforces correct result type
overlay.close({ submitted: true, data: formData })

// When opening
const result = await openOverlay<FormData, FormResult>(FormDialog, {
  data: { name: 'John', email: '[email protected]' }
})

// TypeScript knows result.data has submitted and data properties
if (result.data.submitted) {
  console.log(result.data.data)
}

Integration with UI Libraries

reka-ui (Recommended)

✅ Supports multiple stacked overlays

<script setup lang="ts">
import { useCurrentOverlay } from 'overlay-manager-vue'
import { Dialog, DialogContent } from 'reka-ui'

const overlay = useCurrentOverlay()
</script>

<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <!-- Your content -->
    </DialogContent>
  </Dialog>
</template>

shadcn-vue (Recommended)

✅ Supports multiple stacked overlays

# Initialize shadcn-vue
pnpm dlx shadcn-vue@latest init

# Add dialog component
pnpm dlx shadcn-vue@latest add dialog
<script setup lang="ts">
import { useCurrentOverlay } from 'overlay-manager-vue'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'

const overlay = useCurrentOverlay()
</script>

<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{{ overlay.data.title }}</DialogTitle>
        <DialogDescription>{{ overlay.data.message }}</DialogDescription>
      </DialogHeader>
      <!-- Your content -->
    </DialogContent>
  </Dialog>
</template>

Radix Vue (Limited)

⚠️ AlertDialog does NOT support stacking - Only use Dialog

<script setup lang="ts">
import { useCurrentOverlay } from 'overlay-manager-vue'
import { Dialog, DialogContent } from 'radix-vue'

const overlay = useCurrentOverlay()
</script>

<template>
  <Dialog :open="overlay.isOpen">
    <DialogContent>
      <!-- Your content -->
    </DialogContent>
  </Dialog>
</template>

Headless UI

<script setup lang="ts">
import { useCurrentOverlay } from 'overlay-manager-vue'
import { Dialog, DialogPanel } from '@headlessui/vue'

const overlay = useCurrentOverlay()
</script>

<template>
  <Dialog :open="overlay.isOpen" @close="overlay.dismiss()">
    <DialogPanel>
      <!-- Your content -->
    </DialogPanel>
  </Dialog>
</template>

Migration from Beta

If you're upgrading from beta versions (0.3.x), please see the Migration Guide.

Key Breaking Changes in 1.0.0

  1. Dialog Requirement: Must use Dialog (not AlertDialog) for stacking support
  2. Dependencies: Requires reka-ui and @radix-icons/vue
  3. Full Reactivity: All context properties are now reactive (computed refs)
  4. TypeScript Generics: Better type inference with useOverlayContext<TData, TResult>()

Comparison with overlay-rc (React)

| Feature | overlay-vue | overlay-rc | |---------|-------------|------------| | Framework | Vue 3 | React | | API Style | Hooks (Composables) | Hooks | | Context Access | useCurrentOverlay() | useOverlay() inside overlay | | Reactivity | Vue Computed Refs | React State | | Portal | Internal (no Teleport needed) | React Portal | | Type Safety | ✅ Full TypeScript | ✅ Full TypeScript | | Promise-based | ✅ | ✅ | | Stacked Overlays | ✅ (with Dialog) | ✅ | | Lifecycle Callbacks | ✅ | ✅ |

Testing

Run tests with coverage:

npm test
# or
npm run test:coverage

Current coverage: 97%+

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting PRs.

Related Projects

Changelog

See CHANGELOG.md for version history.