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/modalThe 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.jsonThe internal @easy-shadcn/async-button component and the @easy-shadcn/command-modal package are installed automatically alongside modal.
Usage
Modal
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
Modal
| Prop | Type | Default | Description |
|---|---|---|---|
title / description | ReactNode | - | Header slots. The header renders only when one of them is present. |
children | ReactNode | - | Modal body. |
footer | ReactNode | - | 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 / cancelText | ReactNode | "OK" / "Cancel" | Default footer button labels. |
confirmProps / cancelProps | Omit<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. |
trigger | ReactElement | - | Rendered as the DialogTrigger. |
showCloseButton | boolean | true | The top-right close button. |
disablePointerDismissal | boolean | false | Prevents 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:
| Prop | Type | Default | Description |
|---|---|---|---|
showCancel | boolean | true | Hide 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>— resolvestrueon confirm,falseon 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.
titleis optional in the types, but a dialog without a title has no accessible name — pass one (or render your ownDialogTitlevia the primitives) in production. - For layouts beyond these slots (multi-step wizards, custom headers, side sheets), compose the
components/ui/dialogprimitives directly.