Alert Modal
React overlayProps-driven shortcut for confirm and destructive prompts — pass `title`, `description`, `actionLabel`, and a `tone` and you get a fully styled modal with a leading icon ring, action button, and optional async loading state. Skip it and use Dialog when you need a free-form modal instead.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/alert-modal.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/alert-modal.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/alert-modal.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/alert-modal.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/alert-modal
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
open Controlled open state. Pair with `onOpenChange`. | boolean | — | optional |
onOpenChange | (open: boolean) => void | — | optional |
title Title rendered in the header. Override with the `title` node if you need markup. | React.ReactNode | — | optional |
description Description rendered under the title. | React.ReactNode | — | optional |
actionLabel Label for the primary action button. | React.ReactNode | — | optional |
cancelLabel Label for the cancel button. Pass null to hide. | React.ReactNode | null | — | optional |
tone Visual tone — colors the icon and action button. | 'default' | 'destructive' | 'success' | 'warning' | — | optional |
icon Quick icon shortcut, a custom icon component, or a rendered node. | AlertIcon | LucideIcon | React.ReactNode | null | — | optional |
loading Show a spinner on the action button and disable both buttons. | boolean | — | optional |
actionDisabled Disable the primary action without a spinner. | boolean | — | optional |
className | string | — | optional |
onAction | (event: React.MouseEvent) => void | — | optional |
onCancel | (event: React.MouseEvent) => void | — | optional |
trigger Element that opens the modal. Rendered as the trigger when provided. | React.ReactNode | — | optional |
children Optional free-form body content between the header and the action row. | React.ReactNode | — | optional |
actions Replace the default action/cancel button row. | React.ReactNode | — | optional |
Dependencies
Files (2)
-
components/ui/alert-modal/alert-modal.tsx 5.7 kB
'use client' import * as React from 'react' import { CircleAlert, CircleCheck, Info, TriangleAlert, type LucideIcon } from 'lucide-react' import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' type AlertIcon = 'info' | 'warning' | 'error' | 'success' export interface AlertModalProps { /** Controlled open state. Pair with `onOpenChange`. */ open?: boolean onOpenChange?: (open: boolean) => void /** Title rendered in the header. Override with the `title` node if you need markup. */ title?: React.ReactNode /** Description rendered under the title. */ description?: React.ReactNode /** Label for the primary action button. */ actionLabel?: React.ReactNode /** Label for the cancel button. Pass null to hide. */ cancelLabel?: React.ReactNode | null /** Visual tone — colors the icon and action button. */ tone?: 'default' | 'destructive' | 'success' | 'warning' /** Quick icon shortcut, a custom icon component, or a rendered node. */ icon?: AlertIcon | LucideIcon | React.ReactNode | null /** Show a spinner on the action button and disable both buttons. */ loading?: boolean /** Disable the primary action without a spinner. */ actionDisabled?: boolean className?: string onAction?: (event: React.MouseEvent) => void onCancel?: (event: React.MouseEvent) => void /** Element that opens the modal. Rendered as the trigger when provided. */ trigger?: React.ReactNode /** Optional free-form body content between the header and the action row. */ children?: React.ReactNode /** Replace the default action/cancel button row. */ actions?: React.ReactNode } const builtInIcons: Record<AlertIcon, LucideIcon> = { info: Info, success: CircleCheck, warning: TriangleAlert, error: CircleAlert, } function isAlertIcon(value: unknown): value is AlertIcon { return value === 'info' || value === 'success' || value === 'warning' || value === 'error' } const iconColorClasses: Record<NonNullable<AlertModalProps['tone']>, string> = { destructive: 'text-destructive', success: 'text-success', warning: 'text-warning', default: 'text-muted-foreground', } const actionToneClasses: Record<NonNullable<AlertModalProps['tone']>, string> = { destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', success: 'bg-success text-success-foreground hover:bg-success/90', warning: 'bg-warning text-warning-foreground hover:bg-warning/90', default: '', } const AlertModal = ({ open, onOpenChange, title = '', description = '', actionLabel = 'Continue', cancelLabel = 'Cancel', tone = 'default', icon = null, loading = false, actionDisabled = false, className, onAction, onCancel, trigger, children, actions, }: AlertModalProps) => { const isControlled = open !== undefined const [internalOpen, setInternalOpen] = React.useState<boolean>(open ?? false) const effectiveOpen = isControlled ? open : internalOpen function setOpen(value: boolean) { if (!isControlled) setInternalOpen(value) onOpenChange?.(value) } const resolvedIcon = React.useMemo<React.ReactNode>(() => { if (!icon) return null if (isAlertIcon(icon)) { const IconComp = builtInIcons[icon] return <IconComp className="size-5" /> } if (typeof icon === 'function') { const IconComp = icon as LucideIcon return <IconComp className="size-5" /> } return icon }, [icon]) function handleAction(e: React.MouseEvent) { if (loading || actionDisabled) { e.preventDefault() return } onAction?.(e) } function handleCancel(e: React.MouseEvent) { onCancel?.(e) } return ( <Dialog open={effectiveOpen} onOpenChange={setOpen}> {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>} <DialogContent role="alertdialog" showCloseButton={false} className={cn(className)} > <div className="flex flex-col gap-2 text-center sm:text-left"> {resolvedIcon && ( <div className={cn( 'bg-muted mb-2 flex size-10 items-center justify-center rounded-full', iconColorClasses[tone], )} > {resolvedIcon} </div> )} <DialogTitle className="text-lg font-semibold">{title}</DialogTitle> {description && <DialogDescription className="text-muted-foreground text-sm">{description}</DialogDescription>} </div> {children && <div className="text-sm">{children}</div>} <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> {actions ?? ( <> {cancelLabel != null && ( <DialogClose asChild> <Button variant="outline" disabled={loading} onClick={handleCancel}> {cancelLabel} </Button> </DialogClose> )} <Button disabled={loading || actionDisabled} aria-busy={loading} className={actionToneClasses[tone]} onClick={handleAction} > {loading && ( <span className="mr-2 inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" aria-hidden="true" /> )} {actionLabel} </Button> </> )} </div> </DialogContent> </Dialog> ) } AlertModal.displayName = 'AlertModal' export { AlertModal } -
components/ui/alert-modal/index.ts 0.1 kB
export { AlertModal, type AlertModalProps } from './alert-modal'
Raw manifest: https://react.uipkge.dev/r/react/alert-modal.json