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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@react-overlay-manager/core

v0.4.1

Published

A lightweight React overlay management library with full TypeScript support

Downloads

1,181

Readme

React Overlay Manager

A lightweight, type-safe overlay system for React with a zero dependency and built-in DevTools.

  • 📦 Headless – bring your own styles / animations
  • 🔒 Fully typed – compile-time safety for props and results
  • 🛠 DevTools – inspect the overlay stack in development
  • ⚡️ Fast – minimal state, useSyncExternalStore under the hood
  • 📏 Small – ~2.6 kB gzipped

Table of Contents


Installation

pnpm add @react-overlay-manager/core           # or yarn / npm

Quick Start

1. Define an overlay component

Use the defineOverlay helper for full type-safety. It injects props like visible (for animations) and close (to return a result).

// src/components/ConfirmDialog.tsx
import { defineOverlay } from '@react-overlay-manager/core';
import cx from 'clsx';

interface ConfirmDialogProps {
  message: string;
}

export const ConfirmDialog = defineOverlay<ConfirmDialogProps, boolean>(
  ({ message, visible, close }) => {
    const dialogClass = cx(
      '... transition-opacity duration-300',
      visible
        ? 'opacity-100 pointer-events-auto'
        : 'opacity-0 pointer-events-none'
    );

    return (
      <div className={dialogClass}>
        <div className="...">
          <p>{message}</p>
          <button onClick={() => close(true)}>Confirm</button>
          <button onClick={() => close(false)}>Cancel</button>
        </div>
      </div>
    );
  }
);

2. Create a manager

A manager holds the registry of your overlays.

// src/services/overlayManager.ts
import { createOverlayManager } from '@react-overlay-manager/core';
import { ConfirmDialog } from '../components/ConfirmDialog';

export const overlayManager = createOverlayManager({
  confirm: ConfirmDialog,
});

3. Render the manager at your app's root

The <OverlayManager> component is responsible for rendering your overlays into the DOM. You must always include it in your app's root.

// src/App.tsx
import { OverlayManager } from '@react-overlay-manager/core';
import { overlayManager } from './services/overlayManager';
import { MyPage } from './MyPage';

export default function App() {
  return (
    <>
      <MyPage />
      <OverlayManager manager={overlayManager} />
    </>
  );
}

Note: For overlays without CSS animations, you may want to add defaultExitDuration={0} to ensure they are removed from the DOM after closing. See the Exit Behavior section for details.

4. Open an overlay from anywhere

Call manager.open() from any component, hook, or service. It's fully async and type-safe.

// inside any component / service
import { overlayManager } from '../services/overlayManager';

async function deleteItem() {
  const confirmed = await overlayManager.open('confirm', {
    message: 'Delete this item?',
  }); // `confirmed` is typed as `boolean`

  if (confirmed) {
    // ...delete logic
  }
}

5. Open a component directly (no registry needed)

You can also open components on-the-fly without registering them first.

import { overlayManager } from '../services/overlayManager';
import { TempDialog } from '../components/TempDialog';

await overlayManager.open(TempDialog, { title: 'One-off dialog' });

Alternative: Simplified Setup (No Registry)

If you don't intend to use a component registry and only want to open components directly, you can import a pre-configured, shared manager instance.

You still must render the <OverlayManager> component and pass this default instance to it.

// src/App.tsx
import { OverlayManager, overlays } from '@react-overlay-manager/core';
import { MyPage } from './MyPage';

export default function App() {
  return (
    <>
      <MyPage />
      {/* Render the manager, passing the default 'overlays' instance */}
      <OverlayManager manager={overlays} />
    </>
  );
}

// Then, from any other file:
import { overlays } from '@react-overlay-manager/core';
import { MyDialog } from './components/MyDialog';

function handleClick() {
  overlays.open(MyDialog, { title: 'Hello' });
}

API Reference

Manager Methods

| Method | Signature | Notes | | :------------------ | :----------------------- | :---------------------------------------------------------------------------------------- | | open | open(keyOrComp, opts?) | Opens an overlay. Returns a PromiseWithId that resolves when close(result) is called. | | hide | hide(id) | Hides an overlay by setting visible=false. The component remains mounted. | | show | show(id) | Re-shows a hidden overlay. Throws OverlayNotFoundError if the ID is invalid. | | update | update(id, props) | Merges new props into an existing overlay, triggering a re-render. | | closeAll | closeAll() | Closes all open overlays, respecting their individual exit animations. | | isOpen | isOpen(id) | Returns true if an overlay with the given ID exists in the manager. | | getInstance | getInstance(id) | Returns the runtime instance ({ id, props, visible, ... }) or undefined. | | getInstancesByKey | getInstancesByKey(key) | Returns an array of all instances opened from a specific registry key. | | getOpenCount | getOpenCount() | Returns the number of overlays currently in the stack. |

Injected Overlay Props

These props are automatically passed to every overlay component.

| Prop | Type | Purpose | | :----------------- | :------------------ | :------------------------------------------------------------------------------ | | id | OverlayId | A unique, stable identifier for the overlay instance. | | visible | boolean | true when the overlay should be visible. Drives enter/exit animations. | | hide() | () => void | Hides the overlay without unmounting it or resolving its promise. | | close() | (result?) => void | Resolves the open() promise and begins the exit/removal process. | | onExitComplete() | () => void | Manually tells the manager the exit animation is done, causing instant removal. |


<OverlayManager /> Props

| Prop | Type | Default | Purpose | | :-------------------- | :----------------------------- | :-------------------------------------- | :------------------------------------------------------------------------------------------------------- | | manager | OverlayManagerCore | — | The manager instance created by createOverlayManager (or the shared overlays). | | zIndexBase | number | 100 | Base z-index applied to the first overlay container; each subsequent overlay uses base + index. | | defaultExitDuration | number | null | undefined | Global fallback for exit removal. 0 = remove immediately. null = disable timer, use events/callback. | | portalTarget | HTMLElement | null | document.body (client) / null (SSR) | Default portal element for all overlays. Can be overridden per open() call. | | stackingBehavior | 'stack' | 'hide-previous' | 'hide-previous' | Global stacking mode; can be overridden per open() call. |


open() Options (OpenOptions)

The second argument to open() merges your component props with a few manager options:

| Option | Type | Purpose | | :----------------- | :----------------------------- | :---------------------------------------------------------------------------------------------- | | id | OverlayId | Optional explicit ID. If omitted, a unique one is generated. | | exitDuration | number | null | Per-instance exit timer. 0 = remove immediately. null = disable timer, use events/callback. | | portalTarget | HTMLElement | null | Per-instance portal target. Overrides <OverlayManager portalTarget={...} />. | | stackingBehavior | 'stack' | 'hide-previous' | Per-instance stacking mode. Overrides global stackingBehavior. |

React Hook: useOverlayStore

For building custom UI that reacts to the overlay state, useOverlayStore provides an efficient way to subscribe to changes. It's powered by useSyncExternalStore and only triggers re-renders when the selected state changes.

import { useOverlayStore } from '@react-overlay-manager/core';
import { overlayManager } from './services/overlayManager';

function GlobalBlocker() {
  const isAnyOverlayOpen = useOverlayStore(
    overlayManager,
    (state) => state.overlayStack.length > 0
  );

  // Block background scroll when any overlay is open
  useEffect(() => {
    document.body.style.overflow = isAnyOverlayOpen ? 'hidden' : 'auto';
  }, [isAnyOverlayOpen]);

  return null;
}

Exit Behavior & Animations

When you call close(), the manager sets visible = false and waits to unmount the component. By default, if you do not provide any exitDuration, the manager relies on CSS events to remove the overlay.

Unmounting happens on the first of these events to occur:

  1. CSS Event (Default): A transitionend or animationend event fires on the overlay's root container. This is the "happy path" for CSS-based animations.
  2. Timer: A timeout completes. This is a fallback or primary method if you don't use CSS animations.
  3. Manual Callback: You explicitly call the injected onExitComplete() function. This is required for animation libraries like Framer Motion.

Warning: If your component has no CSS transitions/animations on its root element, and you don't set an exitDuration/defaultExitDuration, the overlay will become invisible after close() but will never be removed from the DOM. To avoid this, either add a CSS transition, set an exitDuration/defaultExitDuration, or call onExitComplete() manually.

The precedence for timers is:

  1. options.exitDuration === null: Disables the timer completely. Relies solely on CSS events or onExitComplete.
  2. options.exitDuration (number): Uses the per-instance duration.
  3. <OverlayManager defaultExitDuration={...} />: Uses the global fallback duration.

Important: The manager listens for transitionend/animationend on the container <div> it renders, not on your component's nested elements. These events do bubble, so child animations will usually be detected. Some libraries or patterns may not emit native events; in that case, set a timer or call onExitComplete() manually. The library safely handles multiple redundant events, so you don't have to worry about race conditions.


Stacking Behavior

You can control how overlays behave when new ones are opened on top of them.

The precedence is: open() options > <OverlayManager /> prop > default ('hide-previous').

| Behavior | Description | Use Case | | :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | | 'hide-previous' (Default) | Opening a new overlay sets visible = false on the one below it. Closing the top one automatically re-shows the previous one. | Modal dialogs, where only one should be interactive at a time. | | 'stack' | New overlays open on top, and previous ones remain visible. | Toasts, notifications, or non-modal pop-ups. |

Example:

// Globally set all overlays to stack
<OverlayManager manager={overlayManager} stackingBehavior="stack" />;

// But override for a specific modal
overlayManager.open('confirm', {
  message: 'Are you sure?',
  stackingBehavior: 'hide-previous', // This one will hide overlays beneath it
});

DevTools

Install the DevTools package to inspect your overlay stack in development.

pnpm add -D @react-overlay-manager/devtools

Render the component next to your OverlayManager. It automatically does nothing in production.

import { OverlayManagerDevtools } from '@react-overlay-manager/devtools';

function App() {
  return (
    <>
      <OverlayManager manager={overlayManager} />
      {/* DevTools will only render in development builds */}
      <OverlayManagerDevtools manager={overlayManager} />
    </>
  );
}

Toggle with Ctrl/Cmd + Shift + O.

Warning: Ensure you pass the exact same manager instance to both <OverlayManager /> and <OverlayManagerDevtools />. Using different instances is a common cause for the DevTools appearing empty. Props shown in DevTools may contain sensitive data; its use is intended for development only.


Examples

CSS Transitions

A minimal example using pure CSS class toggle to drive animations. The manager's built-in transitionend listener handles removal automatically because the transition property is on the root element.

// Spinner.tsx
import { defineOverlay } from '@react-overlay-manager/core';
import './spinner.css';

export const Spinner = defineOverlay<{}, void>(({ visible }) => {
  // Apply 'enter' or 'exit' class based on the `visible` prop
  return (
    <div className={`backdrop ${visible ? 'enter' : 'exit'}`}>
      <div className="spinner" />
    </div>
  );
});
/* spinner.css */
/* The transition must be on the root element that OverlayItem renders */
.backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.3);
  opacity: 0;
  transition: opacity 200ms ease; /* This transition is key */
}
.backdrop.enter {
  opacity: 1;
}
.backdrop.exit {
  opacity: 0;
}
/* ... spinner styles ... */

Framer Motion

For animation libraries, use AnimatePresence and call onExitComplete when the animation finishes. This gives you precise control and instant removal.

// MotionDialog.tsx
import { defineOverlay } from '@react-overlay-manager/core';
import { AnimatePresence, motion } from 'framer-motion';

export const MotionDialog = defineOverlay<{ title: string }, void>(
  ({ title, visible, close, onExitComplete }) => {
    return (
      <AnimatePresence onExitComplete={onExitComplete}>
        {visible && <motion.div /* ... */>{/* ... */}</motion.div>}
      </AnimatePresence>
    );
  }
);

To rely solely on onExitComplete, disable the fallback timer:

// Option 1: Per-call
await overlayManager.open(MotionDialog, {
  title: 'Welcome',
  exitDuration: null, // Disables the timer for this instance
});

// Option 2: Globally (recommended for animation libraries)
<OverlayManager manager={overlayManager} defaultExitDuration={null} />;

TypeScript Guide

Async/Await and PromiseWithId

The open() method returns a PromiseWithId<TResult>, which is a standard Promise with an added id property. This lets you access the overlay's ID immediately, even if you await the result later.

// Get the ID synchronously, then await the result
const promise = overlayManager.open('confirm', { message: 'Proceed?' });
const id = promise.id; // `id` is typed as OverlayId

const ok = await promise; // `ok` is typed as boolean

// Or with async/await, though you lose direct access to the promise object
const result: boolean = await overlayManager.open('confirm', {
  message: 'Again?',
});

Compile-time Errors for Wrong Props

The manager enforces props at compile time, preventing common bugs.

// Assuming 'confirm' expects: { message: string }
overlayManager.open('confirm', {
  message: 123, // ❌ Type 'number' is not assignable to type 'string'.
  unknownProp: true, // ❌ Object literal may only specify known properties.
});

Advanced Patterns

Central Registry with React.lazy

For better code-splitting, use React.lazy in your registry. Wrap your <OverlayManager> in a single <Suspense> at the app root to handle loading states without flicker.

// overlays.ts
import { createOverlayManager } from '@react-overlay-manager/core';
import React, { lazy } from 'react';

export const overlays = createOverlayManager({
  confirm: lazy(() => import('./dialogs/ConfirmDialog')),
  settings: lazy(() => import('./dialogs/SettingsModal')),
});

// App.tsx
<Suspense fallback={<GlobalSpinner />}>
  <OverlayManager manager={overlays} />
</Suspense>;

Using open() without await

Sometimes you want to fire-and-forget an overlay, like a loading spinner. You can grab the id to close it programmatically later.

// Show a spinner and don't wait for a result
const spinner = overlayManager.open('spinner');

try {
  await someAsyncTask();
} finally {
  // Close the spinner by its ID when the task is done
  overlayManager.getInstance(spinner.id)?.close();
}

Nested overlays with the injected manager (concise and type-safe)

Overlays receive a manager prop. Use it to open other overlays (nested modals) and keep key-based typing by narrowing with manager.as<...>(). This avoids circular imports when the overlay is part of the same registry.

  • Why
    • Avoids circular types/imports
    • Keeps nested open('key', ...) fully type-safe

Define your manager in a separate module and export its type:

import { createOverlayManager } from '@react-overlay-manager/core';
import { ValidationModal } from '../components/ValidationModal';
// ...other overlays

export const overlayManager = createOverlayManager({
  validationModal: ValidationModal,
  // confirmModal: ConfirmModal, ...
});

export type AppOverlayManager = typeof overlayManager;
import type { AppOverlayManager } from '../services/overlayManager';

export const ValidationModal = defineOverlay<ValidationModalProps, void>(
  ({ manager, ...props }) => {
    manager
      .as<AppOverlayManager['registry']>()
      .open('confirmModal', { /* ... */ });

    // or open by component directly (no key)
    // manager.open(ConfirmModal, { /* ... */ });
    return null;
  }
);

You can also dynamically import the manager to avoid circular imports:

```ts
const { overlayManager } = await import('../services/overlayManager');
overlayManager.open('confirmModal', { /* ... */ });

Edge Cases & Error Handling

  • OverlayAlreadyOpenError: Thrown if you call open() with an id that is already visible.
    • Solution: Omit the id to let the manager auto-generate a unique one. Alternatively, calling open() with the ID of a hidden overlay will show and update it instead of throwing an error.
  • OverlayNotFoundError: Thrown if you call hide(), show(), or update() on an ID that has already been closed and removed.
    • Solution: In complex async flows, guard your calls: if (manager.isOpen(id)) manager.hide(id); or wrap them in a try/catch block.

SSR / Next.js

The library is SSR-safe. It avoids accessing window or document on the server.

  • portalTarget: On the server, overlays render inline as defaultPortalTarget is null. On the client, it defaults to document.body. You can provide a stable portal element for better DOM structure.
  • Code-splitting: Use next/dynamic to prevent overlay components from being included in the initial server bundle.
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        {/* A dedicated portal target for overlays */}
        <div id="overlay-portal" />
      </body>
    </html>
  );
}

// app/Providers.tsx
('use client');
import { OverlayManager } from '@react-overlay-manager/core';
import { overlays } from './overlays';

export function Providers({ children }) {
  return (
    <>
      {children}
      <OverlayManager
        manager={overlays}
        portalTarget={document.getElementById('overlay-portal')}
      />
    </>
  );
}

Accessibility

This library is headless and unopinionated about your markup. It is your responsibility to make your overlay components accessible. Key considerations include:

  • Roles: Use role="dialog" or role="alertdialog".
  • Labels: Provide an accessible name with aria-labelledby and/or aria-describedby.
  • Modality: Use aria-modal="true" for modal dialogs.
  • Focus Management: Trap focus within the overlay while it's open and return focus to the trigger element when it closes.
  • Keyboard Navigation: Allow closing with the Escape key.

Consider using the native HTML <dialog> element.


Troubleshooting

  1. Overlay doesn't disappear after animation:
    • Ensure your CSS transition or animation is on the root element of your overlay component.
    • If animations are nested or you don't use CSS animations, you must either set an exitDuration/defaultExitDuration or call onExitComplete() manually.
  2. OverlayAlreadyOpenError:
    • You are trying to open() an overlay with an id that is already visible. Let the manager generate IDs automatically by omitting the id option.
  3. Overlays appear behind other content:
    • Check for parent elements with a z-index and position that create a new stacking context. The <OverlayManager zIndexBase={...} /> prop can help, but portals are the best solution. Render overlays into a dedicated portal element at the end of <body>.

Bundling & Versioning

  • The library ships with CJS and ESM formats, with type definitions (.d.ts).

  • react and react-dom are listed as external dependencies.

  • The current version is available as an export: import { version } from '@react-overlay-manager/core'.

Quality & Coverage

  • Runtime tests: 66 tests across 22 files, with overall coverage: Statements 96.64%, Branches 85.25%, Functions 93.67%, Lines 96.64%.
  • Type-level tests (tsd): 15 focused specs across core and devtools verifying generics and API contracts:
    • open() overloads and argument optionality (by key and by component)
    • OverlayManagerProps shape and constraints
    • Helper types: ComponentProps<T>, OverlayResult<T>
    • AnyOverlayInstance narrowing and typed instances
    • Event typing (ManagerEvent<T>) and default manager usage
    • Branded OverlayId in OpenOptions.id

These checks run in CI to prevent regressions and ensure the library remains safe to adopt.

License

MIT