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

use-overlay-stack

v0.1.0

Published

One Sheet. One Dialog. One AlertDialog. Open any of them from anywhere in your React app without prop-drilling state or fighting nested z-index.

Readme

use-overlay-stack

One Sheet. One Dialog. One AlertDialog. Open any of them from anywhere in your React app — no prop-drilling open state through five components, no two features both rendering a <Dialog> and fighting over z-index.

const { setDialog } = useOverlayStack()

<Button onClick={() => setDialog(<EditAccountForm accountId={id} />)}>
  Edit
</Button>

That's it. No local useState, no <Dialog open={open}> wrapper in this component, no worrying whether some parent already has one mounted.

The problem

In most React codebases, every feature that needs a modal ends up doing this:

function AccountRow({ account }) {
  const [editOpen, setEditOpen] = useState(false)
  const [deleteOpen, setDeleteOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setEditOpen(true)}>Edit</Button>
      <Button onClick={() => setDeleteOpen(true)}>Delete</Button>

      <Dialog open={editOpen} onOpenChange={setEditOpen}>
        <EditAccountForm accountId={account.id} onDone={() => setEditOpen(false)} />
      </Dialog>
      <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
        ...
      </AlertDialog>
    </>
  )
}

Multiply that by every row, every table, every feature in the app, and you get: dozens of near-identical open/onOpenChange pairs, dialogs nested three components deep from where the trigger button lives, and two different features occasionally mounting a <Dialog> each — so you get two overlays stacked on top of each other with mismatched backdrops.

use-overlay-stack mounts exactly one Sheet, one Dialog, and one AlertDialog for your whole app (via a provider near the root), and gives you an imperative API to fill and open them from anywhere:

function AccountRow({ account }) {
  const { setDialog, setAlertDialog } = useOverlayStack()

  return (
    <>
      <Button onClick={() => setDialog(<EditAccountForm accountId={account.id} />)}>
        Edit
      </Button>
      <Button
        onClick={() =>
          setAlertDialog({
            title: 'Delete this account?',
            variant: 'destructive',
            onConfirm: () => deleteAccount(account.id),
          })
        }
      >
        Delete
      </Button>
    </>
  )
}

No local state, no JSX for the overlay itself sitting next to the trigger, and it's structurally impossible for two features to both mount a dialog at once — there's only one.

Why three fixed slots, not an arbitrary stack

It's tempting to build a fully generic push()/pop() overlay stack that can nest any number of any kind of overlay. In practice almost every app needs exactly this: one sheet, one dialog, one confirm/alert dialog, and occasionally two of those open at once (e.g. a confirm alert on top of a dialog). Three independent slots give you that combination for free, with an API small enough to hold in your head — and no risk of an accidental infinite stack of nested modals. If you outgrow this, you've outgrown this library; that's fine.

Install

npm install use-overlay-stack

React 18+ only peer dependency. No UI library dependency — you bring your own Sheet, Dialog, and AlertDialog components (shadcn/ui, Radix primitives, Headless UI, Chakra, your own).

Setup

Wrap your app once, near the root, passing in your overlay components:

// app/providers.tsx
import { OverlayStackProvider } from 'use-overlay-stack'
import { Sheet } from '@/components/ui/sheet'
import { Dialog } from '@/components/ui/dialog'
import { AlertDialog } from '@/components/ui/alert-dialog'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <OverlayStackProvider sheet={Sheet} dialog={Dialog} alertDialog={AlertDialog}>
      {children}
    </OverlayStackProvider>
  )
}

Your components need to accept open: boolean and onOpenChange: (open: boolean) => void — which shadcn/ui's Sheet and Dialog already do. The alertDialog component additionally receives the fields from AlertDialogOptions (title, description, confirmText, cancelText, variant, onConfirm, onCancel) as props, so you write one small adapter component around your alert dialog primitive that wires those into its buttons. See examples/shadcn-adapter.tsx for a complete one.

Usage

import { useOverlayStack } from 'use-overlay-stack'

function DeleteButton({ accountId }: { accountId: string }) {
  const { setAlertDialog } = useOverlayStack()

  return (
    <Button
      onClick={() =>
        setAlertDialog({
          title: 'Delete this account?',
          description: 'This cannot be undone.',
          variant: 'destructive',
          confirmText: 'Delete',
          onConfirm: async () => {
            await deleteAccount(accountId)
          },
        })
      }
    >
      Delete
    </Button>
  )
}

Close any overlay imperatively by passing null:

setSheet(null) // closes the sheet
setDialog(null) // closes the dialog
setAlertDialog(null) // closes the alert dialog
closeAll() // closes all three

Content stays mounted for one extra render after you close it, so your Sheet/Dialog component's own exit animation still has something to animate out — it isn't yanked from the DOM the instant open flips to false.

API

<OverlayStackProvider sheet dialog alertDialog>

| Prop | Type | Description | |---|---|---| | sheet | ComponentType<{ open, onOpenChange, children? }> | Your Sheet component. | | dialog | ComponentType<{ open, onOpenChange, children? }> | Your Dialog component. | | alertDialog | ComponentType<{ open, onOpenChange } & AlertDialogOptions> | Your AlertDialog component/adapter. |

useOverlayStack()

| Method | Signature | Description | |---|---|---| | setSheet | (content: ReactNode \| null) => void | Render content in the sheet and open it, or close it with null. | | setDialog | (content: ReactNode \| null) => void | Same, for the dialog. | | setAlertDialog | (options: AlertDialogOptions \| null) => void | Configure and open the alert dialog, or close it with null. | | closeAll | () => void | Close all three at once. |

AlertDialogOptions

interface AlertDialogOptions {
  title: ReactNode
  description?: ReactNode
  confirmText?: string
  cancelText?: string
  variant?: 'default' | 'destructive'
  onConfirm: () => void | Promise<void>
  onCancel?: () => void
}

FAQ

Does this depend on shadcn/ui or Radix?

No. The provider takes your components as props and only assumes they accept open/onOpenChange. It works with any overlay primitive that follows that (very common) contract.

Can I open two sheets at once?

No — that's the point. One sheet slot means one sheet, ever. If a second feature calls setSheet(...) while one is open, it replaces the first. This is almost always what you want; if it isn't, this library isn't the right fit for that screen.

What about Next.js Server Components?

OverlayStackProvider and useOverlayStack are client-side (the provider file is marked 'use client'). Mount the provider in a client component near your root layout, same as any other context provider.

License

MIT © Gurdeep Saroa