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

@propellerads/dialog

v1.0.0

Published

`Dialog` is the blocking window: the page stays where it is, the dialog takes the focus and the keyboard, and nothing else answers until it is done. It is built the shadcn way — one stylesheet, variants picked by [class-variance-authority](https://cva.sty

Readme

Dialog

Dialog is the blocking window: the page stays where it is, the dialog takes the focus and the keyboard, and nothing else answers until it is done. It is built the shadcn way — one stylesheet, variants picked by class-variance-authority, the dialog itself from Base UI — and every value in the stylesheet comes from @propellerads/tokens.

It replaces @propellerads/popup, which is the same window built on styled-components and a hand-rolled portal. See From Popup.

NPM | Github

Installation

bun add @propellerads/dialog

The dialog brings its own stylesheet: the built entry imports it, so importing the component is the whole of it. The tokens are the app's, not the dialog's — they carry the brand, and only the app knows which brand it is:

import '@propellerads/tokens/common.css';
import '@propellerads/tokens/propellerads.css'; // or monetag / propush / zeydoo

The tokens are not optional: no var() in the stylesheet carries a fallback, so without them the dialog has no colours, no spacing and no type. That is on purpose — a page with no design system on it should look like one, not like a dialog that almost works. The stylesheet is also exported as @propellerads/dialog/index.css, for a consumer that extracts CSS by hand.

Use

import Dialog from '@propellerads/dialog';
import Button, {ButtonVariant} from '@propellerads/button';

<Dialog isOpen={isOpen} onClose={close} title="Title" footer={
  <>
    <Button variant={ButtonVariant.Secondary} onClick={close}>Cancel</Button>
    <Button onClick={save}>OK</Button>
  </>
}>
  What the dialog is about.
</Dialog>

Three parts, in the order the design file stacks them: a header with the title and the cross, the content, and a footer whose buttons end at the right edge. A part that is not passed is not rendered — a dialog with no footer has no empty row at the bottom, and no gap where one would have been.

The open state is the product's. Pass isOpen and close it yourself from onClose; nothing closes behind your back. For a dialog whose state nobody else needs, hand it a trigger instead and let it keep its own:

<Dialog trigger={<Button>Open</Button>} title="Title">…</Dialog>

Blocking

Dismissable is the default: the cross, Escape and the backdrop all close the dialog. They are one decision rather than three — a cross that is drawn but does not close, or an Escape that works while the cross is gone, is a dialog lying about itself. Turn it off for a choice that has to be taken, and the cross goes with it:

<Dialog isOpen={isOpen} isDismissable={false} title="Delete the campaign?" footer={…}>

The way out is then whatever the footer offers.

Width

| Width | Max | Where it comes from | | --- | --- | --- | | minimum | 296px | The design file's own size. The default. | | small | 400px | Popup v4's small | | medium | 600px | Popup v4's medium | | big | 1080px | Popup v4's big |

A step is the width the box holds. It gives way to a narrower viewport, and below the mobile breakpoint every step is the width of the screen — but it does not shrink to its content: a ceiling alone would do nothing, because what is inside a dialog stretches to the box rather than the other way round.

When none of the four is the answer, set the custom property instead of reaching for a fifth name:

.onboarding .dialog { --dialog-width: 720px; }

Long content

The box grows with what is inside it, as shadcn's does. A dialog taller than the screen scrolls with the page behind it, header and footer included. Nothing inside is clipped, so a select menu, a date picker or a hint opens past the edge of the box and stays clickable.

Content that is long by nature brings its own scroller, the way shadcn's "Scrollable content" example does. The heading and the buttons then hold their place:

<Dialog isOpen={isOpen} onClose={close} title="Preferred columns" footer={…}>
  <div className="columns">…</div>
</Dialog>
.columns { max-height: 50vh; overflow-y: auto; }

Keep menus out of that scroller: a select inside it is clipped by it.

Props

| Prop | Type | Default | What it does | | --- | --- | --- | --- | | isOpen | boolean | — | Whether the dialog is open. Leave it out for an uncontrolled dialog driven by trigger. | | defaultOpen | boolean | false | The state an uncontrolled dialog starts in. | | onOpenChange | (isOpen: boolean) => void | — | Called whenever the dialog opens or closes, whoever asked for it. | | onClose | () => void | — | Called when the dialog asks to be closed. The shorthand for onOpenChange. | | title | ReactNode | — | The heading, and the dialog's accessible name. | | children | ReactNode | — | What the dialog is about. It does not scroll on its own: see Long content. | | footer | ReactNode | — | The row at the bottom. Buttons end at the right edge. | | width | DialogWidth | 'minimum' | How wide the box may grow. | | isDismissable | boolean | true | Whether the dialog can be dismissed at all: the cross, Escape and the backdrop together. | | closeLabel | string | 'Close' | The cross's accessible name. | | closeIcon | ReactNode | the pack's Close | Replaces the glyph inside the close button. | | trigger | ReactElement | — | What opens the dialog. The element itself becomes the trigger. | | render | ReactElement \| fn | — | Replaces the box with another element. See As a form. | | className | string | — | Goes on the box. | | backdropClassName | string | — | Goes on the backdrop. | | initialFocus | boolean \| RefObject \| fn | true | Where the focus lands when it opens. | | finalFocus | boolean \| RefObject \| fn | true | Where the focus returns when it closes. | | aria-label | string | — | Required when there is no title: a dialog has to say what it is. |

As a form

A submit button in the footer cannot reach a <form> that lives in the content — they are siblings. Render the box as the form instead, and the header, the content and the footer are all inside it:

<Dialog
  isOpen={isOpen}
  onClose={close}
  title="New domain pool"
  render={<form onSubmit={save} />}
  footer={
    <>
      <Button variant={ButtonVariant.Secondary} onClick={close}>Cancel</Button>
      <Button type="submit">Save</Button>
    </>
  }
>
  <InputText name="name" />
</Dialog>

The dialog keeps everything it had — role="dialog", the focus trap, the scroll lock — and gains what a form gives: the submit button works, and Enter in a field submits.

Styling

The custom properties at the top of .dialog are the override API. They need no theme provider — any ancestor of the portal can set them, and so can the dialog's own className:

| Property | Default | What it is | | --- | --- | --- | | --dialog-width | 296px | What the width variant sets. | | --dialog-bg | --color-shade-white-inverter | The surface. Flips with the theme. | | --dialog-fg | --color-shade-black-inverter | The text. | | --dialog-padding | --space-24 | Inside the box. One step less below the mobile breakpoint. | | --dialog-gap | --space-24 | Between header, content and footer. | | --dialog-radius | --radius-24 | The corner. | | --dialog-shadow | --shadow-big | The lift off the backdrop. | | --dialog-backdrop | --over-fullscreen | The dimming behind it. | | --dialog-backdrop-blur | 4px | How far out of focus the page behind goes. 0 takes the blur off. | | --dialog-layer | 3000 | The stacking order, as Popup v4 had it. |

The cross is @propellerads/button — a ghost, icon-only button — repainted by .dialog__close.button { --button-accent: var(--color-quiet) }. It is the quietest thing in the dialog and the action colour belongs to the footer, and the button has no neutral tone yet. Two classes, because the tone class sets that variable on the element itself, so an ancestor cannot reach it.

dialogVariants is exported for a product that renders Base UI's parts itself and only wants the look:

<Dialog.Popup className={dialogVariants({width: 'medium'})} />

Screenshots

The Storybook story at 2×: the playground, then the five shapes — a question, something to agree to, a form, more than fits, and wide.

| Light | Dark | |---|---| | | |

Where this differs from the design file

Three values are deliberately not what the Figma frame draws. They are listed here so nobody "fixes" them back:

| | The file | Here | Why | | --- | --- | --- | --- | | big width | 800px (Popup old/Large) | 1080px | What Popup v4 shipped, and the screens are built against it. | | Corner | 8px | --radius-24 | Decided after the frame was drawn. | | Shadow | Shadow/Middle | --shadow-big | The token for large containers standing over an overlay. |

From Popup

@propellerads/popup is the same window with a different shape. The move is a rename of most props:

| Popup v4 | Dialog | Note | | --- | --- | --- | | header | title | The heading is a heading now — <h2>, read out as the dialog's name. | | children | children | Unchanged. | | footer | footer | Unchanged. | | size | width | Same three sizes under the same names, plus minimum. | | closePopupHandler | onClose | Takes no argument: it is only ever called with false. | | closePopupHandler={undefined} | isDismissable={false} | No cross meant no way to close it. Now that is one decision, and it takes Escape and the backdrop with it. | | wrapperHandler | isDismissable / onClose | A click on the backdrop closes the dialog; isDismissable={false} makes it do nothing. | | theme | — | Dropped. The backdrop follows the page's theme through the tokens. | | popupCustomStyles | className | A class, or one of the custom properties above. | | — | isOpen | Popup was mounted to be shown; the dialog is told. |

Popup rendered only when it was mounted, so the calling code carried an {isVisible && <Popup />}. The dialog takes isOpen and handles its own mounting, which is what gives it the closing animation, the focus return and the scroll lock.