Menu

Change language

GitHub repo

Alert Modal

alert-modalui

Props-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.

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/alert-modal.json
Named registry:npx shadcn@latest add @uipkge-react/alert-modalInstalls to:components/ui/alert-modal/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
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
classNamestring—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

npm dependencies

Includes

Files installed (2)

  • components/ui/alert-modal/alert-modal.tsx6 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) {
        // Keep the dialog open while an async action is in flight.
        if (!value && loading) return
        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)}
            // Alert dialogs must not soft-dismiss on outside click (WAI-ARIA).
            onPointerDownOutside={(e) => e.preventDefault()}
            onInteractOutside={(e) => e.preventDefault()}
          >
            <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.ts0.1 kB

Raw manifest:https://uipkge.dev/r/react/alert-modal.json