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

react-dialog-flow

v0.2.1

Published

Promise-based dialog orchestration for React.

Readme

Open dialogs like functions, await typed results, and compose nested modal flows without scattering boolean state through the page.

Docs and live playground: https://dialog-flow.kangyeol.com

Positioning

Promise-returning dialog APIs are an established pattern in React libraries such as @ebay/nice-modal-react, react-call, and react-async-modal. react-dialog-flow follows that familiar model, with a small headless stack for typed results, dismissal reasons, nested flows, and single-instance entries. Its extra piece is an optional native <dialog> UI primitive that can handle modal behavior, backdrop, scroll lock, focus restoration, and exit-aware transitions when you do not want to bring your own dialog component.

Install

pnpm add react-dialog-flow
npm install react-dialog-flow
yarn add react-dialog-flow

Stability

The package is published on the default npm dist-tag. Async result semantics, stacked dismissal, UI lifecycle defaults, and migration notes are covered by the test suite and documentation. Breaking changes will wait for a new major version.

Quick start

Render one provider near the root of the application, then open dialogs as component-driven flows. Use openAsync when the caller wants to await a typed result instead of wiring boolean state by hand.

import { DialogProvider, useDialog, useDialogInstance } from 'react-dialog-flow';

type User = { id: string; name: string };

function UserSearchDialog() {
  const { close, complete } = useDialogInstance<User | null>();

  return <section role="dialog">
    <h2>Find a user</h2>
    <button onClick={() => complete({ id: 'u_123', name: 'Jiyoon' })}>
      Select Jiyoon
    </button>
    <button onClick={() => close()}>Cancel</button>
  </section>;
}

function ConfirmDialog({ title }: { title: string }) {
  const { close, complete } = useDialogInstance<boolean>();

  return <section role="dialog">
    <h2>{title}</h2>
    <button onClick={() => complete(true)}>Confirm</button>
    <button onClick={() => close('header')}>Cancel</button>
  </section>;
}

function Page() {
  const { openAsync } = useDialog();

  const inviteUser = async () => {
    const user = await openAsync<User | null>(UserSearchDialog, {
      onDismiss: () => null,
    });

    if (!user) return;

    const confirmed = await openAsync<boolean>(ConfirmDialog, {
      title: `Add ${user.name}?`,
      onDismiss: () => false,
    });

    if (confirmed) await addUser(user.id);
  };

  return <button onClick={() => void inviteUser()}>Add user</button>;
}

export function App() {
  return <DialogProvider><Page /></DialogProvider>;
}

DialogProvider creates a portal by default. Set withPortal={false} and render DialogRenderer manually when the application needs to control its placement.

Async results

Use openAsync when the caller only needs a value. You can sequence dialogs as ordinary async work while each dialog owns its own UI and local state.

const user = await openAsync<User | null>(UserSearchDialog, {
  onDismiss: () => null,
});

if (!user) return;

const confirmed = await openAsync<boolean>(ConfirmDialog, {
  title: `Add ${user.name}?`,
  onDismiss: () => false,
});

if (confirmed) await addUser(user.id);

A normal dismissal resolves to false by default, or to the value returned by onDismiss.

Use openAsyncResult when the dismissal reason matters.

const { openAsyncResult } = useDialog();
const result = await openAsyncResult<boolean>(ConfirmDialog);

if (result.status === 'completed') {
  // result.value
} else {
  // result.reason: esc, backdrop, header, or programmatic
}

Both APIs resolve after the entry has completed its exit lifecycle.

Practical flows

Delete confirmation:

const confirmed = await openAsync<boolean>(ConfirmDialog, {
  title: 'Delete project?',
  description: 'This cannot be undone.',
  onDismiss: () => false,
});

if (confirmed) await deleteProject(project.id);

User selection followed by confirmation:

const user = await openAsync<User | null>(UserSearchDialog, {
  onDismiss: () => null,
});

if (!user) return;

const confirmed = await openAsync<boolean>(ConfirmDialog, {
  title: `Invite ${user.name}?`,
  description: `Add ${user.email} to this workspace.`,
  onDismiss: () => false,
});

if (confirmed) await inviteUser(user.id);

Dirty form guard:

<Dialog
  closeOnBackdrop
  shouldClose={async () => {
    if (!formDirty) return true;

    return await openAsync<boolean>(ConfirmDialog, {
      title: 'Discard changes?',
      description: 'Unsaved edits will be lost.',
      onDismiss: () => false,
    });
  }}
>
  ...
</Dialog>

Nested confirmation and flow abort:

const { closeAll, openAsync } = useDialog();

const confirmed = await openAsync<boolean>(ConfirmDialog, {
  title: 'Start import?',
  onDismiss: () => false,
});

if (!confirmed) return;

const overwrite = await openAsync<boolean>(ConfirmDialog, {
  title: 'Overwrite existing records?',
  onDismiss: () => false,
});

if (!overwrite) {
  closeAll();
  return;
}

await runImport();

Optional UI primitive

The stack is headless. Import the UI primitive for a native modal dialog, backdrop, scroll lock, focus restoration, and exit-aware transitions. Minimal base styles are injected automatically so the primitive works without a CSS import. Add react-dialog-flow/ui/style.css only when you want the bundled default theme.

Dialog prefers native showModal(). If the browser refuses that call, it falls back to the open attribute while marking background content inert, hiding it from assistive technology, and keeping focus inside the dialog.

import { Dialog } from 'react-dialog-flow/ui';
import 'react-dialog-flow/ui/style.css';

function ConfirmDialog() {
  const { complete } = useDialogInstance<boolean>();

  return <Dialog closeOnBackdrop closeOnEscape={false}>
    <Dialog.Header>
      <Dialog.Title>Delete project?</Dialog.Title>
    </Dialog.Header>
    <Dialog.Description>This cannot be undone.</Dialog.Description>
    <Dialog.Footer>
      <button onClick={() => complete(true)}>Delete</button>
    </Dialog.Footer>
  </Dialog>;
}

Dialog.Title supplies the accessible name and Dialog.Description is optional supporting text. Use initialFocusRef and finalFocusRef when the default focus placement or restoration is not appropriate.

When closeOnBackdrop is enabled, the backdrop is rendered as a full-viewport button so pointer and keyboard dismissal share the same path. Its accessible label defaults to Close dialog; set backdropCloseLabel for app-specific wording, or use backdropProps when you need lower-level button attributes.

Customize the optional theme through classes, backdropProps, panel, overlay, and CSS custom properties.

Escape closes dialogs by default. Set closeOnEscape={false} when a flow must be completed or dismissed explicitly.

Use shouldClose when a dismissal needs a guard. It receives the close reason and may return a boolean or a promise. Returning or resolving false keeps the dialog open. While the promise is pending, additional close requests for the same dialog are ignored. complete(value) does not run shouldClose.

<Dialog
  closeOnBackdrop
  shouldClose={async (reason) => {
    if (reason !== 'backdrop' || !formDirty) return true;

    return await openAsync<boolean>(ConfirmDialog, {
      title: 'Discard changes?',
      onDismiss: () => false,
    });
  }}
>
  ...
</Dialog>

Bring your own dialog UI

react-dialog-flow is an orchestration layer, not a replacement for your UI system. Use the optional Dialog primitive when you want it, or render Radix, shadcn/ui, or an internal design-system dialog inside the stack entry.

Radix Dialog:

import * as RadixDialog from '@radix-ui/react-dialog';
import { useDialogInstance } from 'react-dialog-flow';

function DeleteProjectDialog({ projectName }: { projectName: string }) {
  const { close, complete } = useDialogInstance<boolean>();

  return (
    <RadixDialog.Root
      open
      onOpenChange={(open) => !open && close('programmatic')}
    >
      <RadixDialog.Portal>
        <RadixDialog.Overlay />
        <RadixDialog.Content>
          <RadixDialog.Title>Delete {projectName}?</RadixDialog.Title>
          <RadixDialog.Description>This cannot be undone.</RadixDialog.Description>
          <button onClick={() => close('programmatic')}>Cancel</button>
          <button onClick={() => complete(true)}>Delete</button>
        </RadixDialog.Content>
      </RadixDialog.Portal>
    </RadixDialog.Root>
  );
}

shadcn/ui style:

import { useDialogInstance } from 'react-dialog-flow';

function ConfirmDialog({ title }: { title: string }) {
  const { close, complete } = useDialogInstance<boolean>();

  return (
    <Dialog open onOpenChange={(open) => !open && close('programmatic')}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline" onClick={() => close('programmatic')}>
            Cancel
          </Button>
          <Button onClick={() => complete(true)}>Confirm</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

When you use the bundled UI primitive, customize it through classes and CSS custom properties:

.danger-dialog {
  --rdf-dialog-panel-background: #111827;
  --rdf-dialog-panel-color: #f9fafb;
  --rdf-dialog-panel-radius: 1.25rem;
  --rdf-dialog-header-gap: 0.75rem;
  --rdf-dialog-close-button-size: 2.25rem;
  --rdf-dialog-close-button-radius: 999px;
  --rdf-dialog-close-button-hover-background: rgb(255 255 255 / 10%);
  --rdf-dialog-close-icon-size: 1.5rem;
  --rdf-dialog-close-icon-stroke-width: 1.8;
}

dialog.rdf-dialog .rdf-dialog__close-icon {
  width: 2rem;
  height: 2rem;
}

Documentation playground

The live documentation and playground are available at https://dialog-flow.kangyeol.com. The local Vite docs app exercises stacking, closeTop, closeAll, Escape handling, async results, guarded dismissal, and the UI primitive. The playground event log keeps the latest 999 entries and can be reset while testing flows.

pnpm docs

Development

pnpm install
pnpm verify

verify runs typecheck, tests, and the library build. The same command runs automatically before packing or publishing.

Project layout

  • src/core: React-independent stack and type definitions.
  • src/react: provider, renderer, hooks, and entry lifecycle integration.
  • src/ui: optional native-dialog UI primitive and styles.
  • tests: reducer and React lifecycle integration coverage.
  • docs: documentation site and live playground.