Easy Shadcn
Components

Modal

Modal and AlertModal flatten shadcn's Dialog / AlertDialog compound components into single components with title, description, and footer props — plus promise-style AlertModal.alert() / AlertModal.confirm() helpers and a full imperative API powered by @easy-shadcn/command-modal.

Installation

With the @easy-shadcn namespace configured:

pnpm dlx shadcn@latest add @easy-shadcn/modal

Or install via the full URL (zero configuration):

pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/modal.json

The internal @easy-shadcn/async-button component and the @easy-shadcn/command-modal package are installed automatically alongside modal.

Usage

The flat onConfirm / onCancel footer needs no controlled state — confirm closes the modal after the (optionally async) handler resolves, and a rejection keeps it open:

import { Modal } from "@/components/easy/modal"

<Modal
  title="Publish post"
  description="The post goes live immediately."
  trigger={<Button>Publish</Button>}
  onConfirm={async () => {
    await publish()
  }}
>
  Body content
</Modal>

AlertModal

A declarative confirm dialog with the same footer semantics:

alert() / confirm() helpers

For the imperative helpers, mount the provider once near the root:

import { Modal } from "@/components/easy/modal"

const App = ({ children }) => (
  <Modal.Provider>{children}</Modal.Provider>
)

Then call them from anywhere — alert() resolves when dismissed, confirm() resolves true / false and never rejects:

import { AlertModal } from "@/components/easy/modal"

await AlertModal.alert({ title: "Saved", description: "All changes stored." })

const ok = await AlertModal.confirm({
  title: "Delete item?",
  description: "This cannot be undone.",
  confirmProps: { variant: "destructive" },
})

Imperative modals with useModalHolder

Any component created with Modal.create can be driven imperatively — useModalHolder additionally lets you update its props after opening:

Current Count: 0

API

PropTypeDefaultDescription
title / descriptionReactNode-Header slots. The header renders only when one of them is present.
childrenReactNode-Modal body.
footerReactNode-Replaces the default footer entirely.
onConfirm / onCancel() => void | Promise<void>-Enables the default OK/Cancel footer. Async handlers show a pending state; a rejection keeps the modal open.
confirmText / cancelTextReactNode"OK" / "Cancel"Default footer button labels.
confirmProps / cancelPropsAsyncButtonProps-Extra props for the default footer buttons (e.g. { variant: "destructive" }).
open / defaultOpen / onOpenChange--Controlled / uncontrolled open state.
triggerReactElement-Rendered as the DialogTrigger.
showCloseButtonbooleantrueThe top-right close button.
disablePointerDismissalbooleanfalsePrevents closing via outside clicks.
onOpenChangeComplete(open: boolean) => void-Base UI's post-transition hook, fired after the open/close animation completes. Forwarded to the underlying Dialog.

Class overrides: className (dialog content), headerClassName, titleClassName, descriptionClassName, contentClassName (body), footerClassName.

AlertModal

Same flat footer contract as Modal, plus:

PropTypeDefaultDescription
showCancelbooleantrueHide the cancel button for single-action alerts.
size"default" | "sm""default"Forwarded to the AlertDialogContent primitive.

AlertModal.alert(props) / AlertModal.confirm(props)

Promise-style helpers (require <Modal.Provider>):

  • alert(props)Promise<void> — a single OK button; resolves when confirmed or dismissed.
  • confirm(props)Promise<boolean> — resolves true on confirm, false on cancel or dismissal (Escape). It never rejects, so fire-and-forget calls are safe.
  • Both accept AlertModalProps (minus the open-state props) and clean up their modal registry entry after closing.

Imperative API on Modal.*

Modal.create, Modal.show, Modal.hide, Modal.remove, Modal.register, Modal.unregister, Modal.useModal, Modal.useModalHolder, and Modal.Provider re-export the public @easy-shadcn/command-modal surface. See the command-modal docs for the full contract.

Notes

  • Accessibility name. title is optional in the types, but a dialog without a title has no accessible name — pass one (or render your own DialogTitle via the primitives) in production.
  • For layouts beyond these slots (multi-step wizards, custom headers, side sheets), compose the components/ui/dialog primitives directly.

On this page