UIPackage

Alert Modal

React overlay
Edit on GitHub

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.

Also available for Vue ->

Installation

$ npx 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