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

@planningcenter/sweetest-alert

v2.1.0

Published

The sweetest alert ever

Readme

Sweetest Alert

A lightweight, React alert/modal dialog component that replaces SweetAlert2 with a custom implementation using native HTML dialog elements.

Installation

yarn add @planningcenter/sweetest-alert

In your product’s main CSS file (ex. app/assets/stylesheets/application.css) add the required CSS imports:

@import "@planningcenter/tapestry/dist/index.css";
@import "@planningcenter/sweetest-alert/style.css";

Peer Dependencies

This package requires the following peer dependencies:

  • react ^18.3.1 || ^19.0.0
  • react-dom ^18.3.1 || ^19.0.0
  • @planningcenter/tapestry >=4
  • @planningcenter/icons ^15.29.1

Using with React 17

The peer dependency range is react/react-dom ^18.3.1 || ^19.0.0 because sweetest-alert statically imports createRoot from react-dom/client, which doesn't exist on React 17. If your app is still on React 17 (as giving was — see planningcenter/giving#8757), you can't upgrade react-dom just for this package, but you can shim the import your bundler resolves instead of the real module.

Add a shim implementing the subset of the react-dom/client API sweetest-alert actually uses (createRoot(container).render()/.unmount()), backed by React 17's real APIs:

// app/javascript/utils/react_dom_client_shim.js
import ReactDOM from "react-dom"

// react-dom/client doesn't exist on React 17. sweetest-alert statically
// imports createRoot from it, which fails at build time otherwise. This
// shim implements just the subset it actually uses so the import resolves
// and behaves the same as it would on React 18. Remove once on React 18
// and this can be dropped in favor of the real react-dom/client.
export function createRoot(container) {
  return {
    render: (element) => {
      ReactDOM.render(element, container)
    },
    unmount: () => ReactDOM.unmountComponentAtNode(container),
  }
}

Then alias react-dom/client to it in your bundler config. For Vite:

// vite.config.mjs
resolve: {
  alias: [
    // ...your other aliases,
    {
      find: "react-dom/client",
      replacement: resolve(__dirname, "app/javascript/utils/react_dom_client_shim.js"),
    },
  ],
},

If you also run tests through Vitest, add the same alias to its config (or to whatever resolve.alias it merges from your Vite config) so component tests resolve the shim too.

Usage

Basic Example

import { SweetestAlert } from "@planningcenter/sweetest-alert"

// Simple notification
SweetestAlert({
  title: "Warning!",
  content: "Watch out, he’s coming to get you!",
})

Confirmation Dialog

SweetestAlert({
  type: "danger",
  title: "Delete Item",
  content: "Are you sure you want to delete this item?",
  onConfirm: () => console.log("Confirmed"),
  onCancel: () => console.log("Cancelled"),
})

Promise-based Usage

SweetestAlert also returns a promise that resolves to { isConfirmed, isDismissed }, so you can use .then()/await instead of (or alongside) onConfirm/onCancel:

const { isConfirmed } = await SweetestAlert({
  type: "danger",
  title: "Delete Item",
  content: "Are you sure you want to delete this item?",
})

if (isConfirmed) {
  deleteItem()
}

Or chain .then() when you can’t (or don’t want to) use await:

SweetestAlert({
  title: "Discard changes?",
  content: "You have unsaved changes that will be lost.",
  confirmButton: "Discard",
}).then(({ isConfirmed }) => {
  if (isConfirmed) {
    discardChanges()
  }
})

isDismissed is handy when you want to react to a cancel or Escape too, not just a confirm:

const { isConfirmed, isDismissed } = await SweetestAlert({
  title: "Leave without saving?",
  content: "Your draft will be lost if you leave now.",
})

if (isConfirmed) {
  navigateAway()
} else if (isDismissed) {
  trackEvent("leave_prompt_dismissed")
}

Async Confirm

If onConfirm returns a promise (or other thenable), the dialog stays open with a loading confirm button and a disabled cancel button until it settles. Resolving closes the dialog and adds the resolved value as value on the result. Escape and close() do nothing while it's pending — cleanup() is the only way to force the dialog closed mid-flight.

const { isConfirmed, value } = await SweetestAlert({
  type: "warning",
  title: "Save changes?",
  content: "The dialog stays open, with a spinner, while the save is in flight.",
  confirmButton: "Save",
  onConfirm: async () => {
    await saveChanges()
    return { savedAt: new Date().toISOString() }
  },
})

if (isConfirmed) {
  console.log("saved", value)
}

If it rejects, the dialog's content is replaced with a generic error message and a single "Okay" button. Dismissing that button — the promise doesn't resolve on rejection itself — resolves { isConfirmed: false, isDismissed: true }. The returned promise never rejects, so a failed confirm can't be mistaken for a success by a caller using .then()/await without a try/catch:

SweetestAlert({
  type: "danger",
  title: "Delete this list?",
  content: "This can't be undone.",
  confirmButton: "Delete",
  onConfirm: () => deleteList(),
})

isDismissed: true now also covers a failed confirm, not just cancel/Escape — if you need to distinguish "the user backed out" from "the action failed," check isConfirmed first, since both cases share isConfirmed: false.

If you don't want the dialog to wait on the returned promise, don't return it:

onConfirm: () => {
  void saveChanges()
}

Alert Types

The component supports five visual types:

// Info alert
SweetestAlert({
  type: "info",
  title: "Information",
  content: "This is an informational message.",
})

// Success alert
SweetestAlert({
  type: "success",
  title: "Success!",
  content: "Operation completed successfully.",
})

// Warning alert (default)
SweetestAlert({
  type: "warning",
  title: "Warning",
  content: "Please proceed with caution.",
})

// Error alert
SweetestAlert({
  type: "error",
  title: "Error",
  content: "Something went wrong.",
})

// Danger alert (for destructive actions)
SweetestAlert({
  type: "danger",
  title: "Delete Account",
  content: "This action cannot be undone.",
  confirmButton: "Delete",
})

Custom Content

You can pass React components as content for rich formatting:

SweetestAlert({
  title: "Custom Content",
  content: (
    <>
      <p>You can include any React elements:</p>
      <ul>
        <li>Lists</li>
        <li>Links</li>
        <li>Formatted text</li>
      </ul>
    </>
  ),
})

Confirmation Input

Reserve this for destructive, hard-to-undo actions — deleting an organization, wiping a list, removing a person. The typing friction is the whole point, and it only stays meaningful if it's rare. Use discretion: on a routine confirm it's just an obstacle, and if people meet it often they learn to type past it without reading. If a plain confirm dialog would do, use a plain confirm dialog.

Pass confirmation to require the user to type an exact phrase before the confirm button enables:

SweetestAlert({
  type: "danger",
  title: `Delete ${list.name}?`,
  content: "This permanently deletes the list and everything in it.",
  confirmButton: "Delete list",
  confirmation: { match: list.name },
  onConfirm: () => destroyList(list),
})

The confirm button stays disabled until the input's value — trimmed, and compared case-sensitively — equals confirmation.match. The input autofocuses when the dialog opens, and pressing Enter in it confirms once it matches. Blurring the field with a non-empty mismatch shows an inline error; it clears as soon as the value matches. This works with any type and composes with hideCancel.

Customize the label or helper text with confirmation.label/confirmation.description:

SweetestAlert({
  type: "danger",
  title: "Delete this organization?",
  content: "Every list, person, and integration underneath it goes too.",
  confirmButton: "Delete organization",
  confirmation: {
    match: organization.name,
    label: "Organization name",
    description: "Case sensitive.",
  },
  onConfirm: () => destroyOrganization(organization),
})

API

Parameters

| Parameter | Description | | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | | title | Required. The main heading text displayed at the top of the modal. | | content | Required. The body content. Accepts plain text or React components (string \| React.ReactNode). | | type | The visual type of alert (info, success, error, danger, warning), affects icon and styling. Defaults to "warning". | | onConfirm | Callback executed when confirm button is clicked. If it returns a promise, the dialog stays open with a loading confirm button until it settles — see Async Confirm. | | onCancel | Callback executed when cancel button is clicked. Not called when onConfirm rejects. | | confirmButton | Custom text for the confirm button. Defaults to "Okay". | | hideCancel | When true, hides the cancel button for simple notifications. Defaults to false. | | confirmation | When set ({ match, label?, description? }), requires the user to type match before the confirm button is enabled. Case-sensitive; surrounding whitespace is ignored. Pressing Enter in the box confirms. An empty or whitespace-only match means no requirement. Reserve for destructive actions — see Confirmation Input. |

Return Value

SweetestAlert returns a promise that resolves to { isConfirmed, isDismissed } when the dialog is closed (via confirm, cancel, Escape, or dismissing the error state after a rejected onConfirm), plus:

| Property | Description | | -------- | ------------------------------------------------------------------------------------------------------------------ | | value | The value onConfirm's promise resolved with. Only present when onConfirm returned a promise and it resolved. |

The returned object is also merged with imperative controls:

| Property | Description | | --------- | ------------------------------------------------------------------------------------------------------------ | | show | Re-opens the dialog (calls the native showModal()). | | close | Closes the dialog (calls the native close()), resolving the promise with isDismissed: true. Does nothing while onConfirm is pending. | | cleanup | Unmounts the React root and removes the dialog from the DOM immediately, regardless of pending state. |

Development

Running the Demo

To run the interactive demo locally:

# Install dependencies
yarn install

# Start the development server
yarn dev

Then open your browser to http://localhost:3000. The demo showcases all alert types and features with interactive buttons.

Running Tests

The project uses Vitest for testing:

# Run tests once
yarn test

# Run tests in watch mode
yarn test --watch

Linting

# Lint JavaScript/TypeScript files
yarn lint:js

# Lint CSS files
yarn lint:css

License

MIT