Alert Dialog
A modal dialog that interrupts the user with important content and expects a response.
The AlertDialog component displays a modal dialog that interrupts the user with important content and expects a response. It includes an overlay backdrop, supports keyboard navigation (Escape to close), and provides subcomponents for title, description, and action buttons. Built from scratch using React and native HTML elements. No UI library dependencies.
Code
TypeScript: Copy this code into components/ui/alert-dialog.tsx:
tsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
interface AlertDialogContextValue {
open: boolean
onOpenChange: (open: boolean) => void
}
const AlertDialogContext = React.createContext<AlertDialogContextValue | undefined>(undefined)
const useAlertDialog = () => {
const context = React.useContext(AlertDialogContext)
if (!context) {
throw new Error("AlertDialog components must be used within AlertDialog")
}
return context
}
interface AlertDialogProps {
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
children: React.ReactNode
}
const AlertDialog = ({ open: controlledOpen, defaultOpen = false, onOpenChange, children }: AlertDialogProps) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const isControlled = controlledOpen !== undefined
const open = isControlled ? controlledOpen : uncontrolledOpen
const handleOpenChange = React.useCallback((newOpen: boolean) => {
if (!isControlled) {
setUncontrolledOpen(newOpen)
}
onOpenChange?.(newOpen)
}, [isControlled, onOpenChange])
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
handleOpenChange(false)
}
}
document.addEventListener("keydown", handleEscape)
return () => document.removeEventListener("keydown", handleEscape)
}, [open, handleOpenChange])
React.useEffect(() => {
if (open) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
return () => {
document.body.style.overflow = ""
}
}, [open])
return (
<AlertDialogContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
{children}
</AlertDialogContext.Provider>
)
}
const AlertDialogTrigger = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, children, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onOpenChange(true)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={className}
{...props}
>
{children}
</button>
)
})
AlertDialogTrigger.displayName = "AlertDialogTrigger"
const AlertDialogContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
const { open, onOpenChange } = useAlertDialog()
if (!open) return null
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onOpenChange(false)
}
}
const content = (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={handleBackdropClick}
>
<div className="fixed inset-0 bg-black/50" />
<div
ref={ref}
className={cn(
"relative z-50 w-full max-w-lg rounded-lg border-2 border-foreground bg-background p-6 neobrutalism-shadow",
className
)}
onClick={(e) => e.stopPropagation()}
{...props}
>
{children}
</div>
</div>
)
return ReactDOM.createPortal(content, document.body)
})
AlertDialogContent.displayName = "AlertDialogContent"
const AlertDialogHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props}
/>
))
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h2
ref={ref}
className={cn("text-lg font-bold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = "AlertDialogTitle"
const AlertDialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName = "AlertDialogDescription"
const AlertDialogFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
))
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogAction = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onOpenChange(false)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 neobrutalism-border neobrutalism-shadow active:translate-x-[2px] active:translate-y-[2px] active:shadow-none bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
className
)}
{...props}
/>
)
})
AlertDialogAction.displayName = "AlertDialogAction"
const AlertDialogCancel = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onOpenChange(false)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 neobrutalism-border neobrutalism-shadow active:translate-x-[2px] active:translate-y-[2px] active:shadow-none border-2 border-foreground bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 mt-2 sm:mt-0",
className
)}
{...props}
/>
)
})
AlertDialogCancel.displayName = "AlertDialogCancel"
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
}JavaScript: Copy this code into components/ui/alert-dialog.jsx:
jsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
const AlertDialogContext = React.createContext(undefined)
const useAlertDialog = () => {
const context = React.useContext(AlertDialogContext)
if (!context) {
throw new Error("AlertDialog components must be used within AlertDialog")
}
return context
}
const AlertDialog = ({ open: controlledOpen, defaultOpen = false, onOpenChange, children }) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const isControlled = controlledOpen !== undefined
const open = isControlled ? controlledOpen : uncontrolledOpen
const handleOpenChange = React.useCallback((newOpen) => {
if (!isControlled) {
setUncontrolledOpen(newOpen)
}
onOpenChange?.(newOpen)
}, [isControlled, onOpenChange])
React.useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape" && open) {
handleOpenChange(false)
}
}
document.addEventListener("keydown", handleEscape)
return () => document.removeEventListener("keydown", handleEscape)
}, [open, handleOpenChange])
React.useEffect(() => {
if (open) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
return () => {
document.body.style.overflow = ""
}
}, [open])
return (
<AlertDialogContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
{children}
</AlertDialogContext.Provider>
)
}
const AlertDialogTrigger = React.forwardRef(
({ className, children, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e) => {
onOpenChange(true)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={className}
{...props}
>
{children}
</button>
)
}
)
AlertDialogTrigger.displayName = "AlertDialogTrigger"
const AlertDialogContent = React.forwardRef(
({ className, children, ...props }, ref) => {
const { open, onOpenChange } = useAlertDialog()
if (!open) return null
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
onOpenChange(false)
}
}
const content = (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={handleBackdropClick}
>
<div className="fixed inset-0 bg-black/50" />
<div
ref={ref}
className={cn(
"relative z-50 w-full max-w-lg rounded-lg border-2 border-foreground bg-background p-6 neobrutalism-shadow",
className
)}
onClick={(e) => e.stopPropagation()}
{...props}
>
{children}
</div>
</div>
)
return ReactDOM.createPortal(content, document.body)
}
)
AlertDialogContent.displayName = "AlertDialogContent"
const AlertDialogHeader = React.forwardRef(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props}
/>
)
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogTitle = React.forwardRef(
({ className, ...props }, ref) => (
<h2
ref={ref}
className={cn("text-lg font-bold", className)}
{...props}
/>
)
)
AlertDialogTitle.displayName = "AlertDialogTitle"
const AlertDialogDescription = React.forwardRef(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
)
AlertDialogDescription.displayName = "AlertDialogDescription"
const AlertDialogFooter = React.forwardRef(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
)
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogAction = React.forwardRef(
({ className, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e) => {
onOpenChange(false)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 neobrutalism-border neobrutalism-shadow active:translate-x-[2px] active:translate-y-[2px] active:shadow-none bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
className
)}
{...props}
/>
)
}
)
AlertDialogAction.displayName = "AlertDialogAction"
const AlertDialogCancel = React.forwardRef(
({ className, onClick, ...props }, ref) => {
const { onOpenChange } = useAlertDialog()
const handleClick = (e) => {
onOpenChange(false)
onClick?.(e)
}
return (
<button
ref={ref}
onClick={handleClick}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 neobrutalism-border neobrutalism-shadow active:translate-x-[2px] active:translate-y-[2px] active:shadow-none border-2 border-foreground bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 mt-2 sm:mt-0",
className
)}
{...props}
/>
)
}
)
AlertDialogCancel.displayName = "AlertDialogCancel"
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
}Usage
TypeScript:
tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
<AlertDialogTrigger asChild>
<Button>Open</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently remove the component
from your Things library installation.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>JavaScript:
jsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
function MyComponent() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button>Open</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.