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

The full URL also works after the @easy-shadcn namespace is configured; the mapping is required to resolve the internal async-button dependency:

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 / cancelPropsOmit<AsyncButtonProps, "children" | "onClick">-Extra props for the default footer buttons (e.g. { variant: "destructive" }). AsyncButton-owned element/semantic keys are excluded too.
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.

Modal owns default-footer behavior, button elements/semantics, and labels. Use onConfirm / onCancel for actions and confirmText / cancelText for labels; those paths cannot be replaced through the button prop bags. Use footer when the whole action row must be custom. Supported AsyncButton props remain available, including variant, loading, disabled, icons, type, and form.

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