Modal
A dialog component for displaying content that requires user attention or interaction. Accessible and keyboard-friendly.
Installation
Terminal
TSXReactTailwind
npx @uiblox/cli add modalVisual Variations
Toggle between three different visual styles: default (standard), centered (compact), and full width (wide).
Standard dialog
Alert dialog
Form modal
Basic Usage
Copy & paste ready
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<ModalClose onClose={() => setOpen(false)} />
<ModalHeader>
<ModalTitle>Edit Profile</ModalTitle>
<ModalDescription>Make changes to your profile here.</ModalDescription>
</ModalHeader>
<ModalContent>
<p>Your profile information goes here.</p>
</ModalContent>
<ModalFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button onClick={() => setOpen(false)}>Save Changes</Button>
</ModalFooter>
</Modal>Confirmation Dialog
Copy & paste ready
const [open, setOpen] = useState(false);
<Button variant="destructive" onClick={() => setOpen(true)}>Delete Item</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<ModalHeader>
<ModalTitle>Are you sure?</ModalTitle>
<ModalDescription>
This action cannot be undone. This will permanently delete your item.
</ModalDescription>
</ModalHeader>
<ModalFooter className="pt-6">
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={() => setOpen(false)}>Delete</Button>
</ModalFooter>
</Modal>Accessibility
Modal dialogs require careful accessibility handling. UIBlox Modal includes focus trapping, escape key handling, and proper ARIA attributes by default.
Keyboard Support
Escape- Close the modalTab- Navigate within modal (trapped)Shift+Tab- Navigate backwards
Built-in Features
- ✓
role="dialog"andaria-modal - ✓Body scroll prevention
- ✓Focus returns to trigger on close
- ✓Backdrop click to dismiss
Accessible Modal Patterns
// Label the modal with aria-labelledby
<Modal open={open} onClose={onClose}>
<ModalHeader>
<ModalTitle id="modal-title">Edit Profile</ModalTitle>
<ModalDescription id="modal-desc">
Make changes to your profile information.
</ModalDescription>
</ModalHeader>
{/* ... */}
</Modal>
// For alert dialogs (requires immediate attention)
<Modal
open={open}
onClose={onClose}
role="alertdialog"
aria-labelledby="alert-title"
aria-describedby="alert-desc"
>
<ModalHeader>
<ModalTitle id="alert-title">Delete Account?</ModalTitle>
<ModalDescription id="alert-desc">
This action is permanent and cannot be undone.
</ModalDescription>
</ModalHeader>
<ModalFooter>
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button variant="destructive" onClick={handleDelete}>
Delete Forever
</Button>
</ModalFooter>
</Modal>
// Auto-focus the first interactive element
<Modal open={open} onClose={onClose}>
<ModalContent>
<Input
autoFocus // Focus moves here when modal opens
placeholder="Enter your name"
/>
</ModalContent>
</Modal>
// Close button with proper label
<ModalClose onClose={onClose} aria-label="Close dialog" />Best Practices
- • Always provide a clear title via
ModalTitle - • Use
role="alertdialog"for important confirmations - • Focus should move to the modal when opened
- • Ensure the close button is keyboard accessible
- • Don't trap users—always provide a way to dismiss
Features
- Press Esc to close
- Click backdrop to close
- Prevents body scroll when open
- Accessible with proper ARIA attributes
- Smooth animations
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | - | Whether the modal is open |
| onClose* | () => void | - | Callback when the modal should close |
| children* | React.ReactNode | - | The modal content |
| className | string | - | Additional CSS classes for the modal container |
Source Code
Copy this code into src/components/ui/modal.tsx:
modal.tsx
TSXReactTailwind
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
}
const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
({ open, onClose, children, className }, ref) => {
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
if (open) {
document.addEventListener("keydown", handleEscape);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "";
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50">
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
<div className="fixed inset-0 flex items-center justify-center p-4">
<div
ref={ref}
role="dialog"
aria-modal="true"
className={cn(
"relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-lg shadow-xl",
"animate-in fade-in-0 zoom-in-95 duration-200",
className
)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
</div>
);
}
);
Modal.displayName = "Modal";
const ModalHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...props} />
)
);
ModalHeader.displayName = "ModalHeader";
const ModalTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h2
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight text-gray-900 dark:text-white", className)}
{...props}
/>
)
);
ModalTitle.displayName = "ModalTitle";
const ModalDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-gray-600 dark:text-gray-400", className)} {...props} />
)
);
ModalDescription.displayName = "ModalDescription";
const ModalContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6", className)} {...props} />
)
);
ModalContent.displayName = "ModalContent";
const ModalFooter = 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 p-6 pt-0", className)}
{...props}
/>
)
);
ModalFooter.displayName = "ModalFooter";
const ModalClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement> & { onClose?: () => void }
>(({ className, onClose, ...props }, ref) => (
<button
ref={ref}
onClick={onClose}
className={cn(
"absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
className
)}
{...props}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<span className="sr-only">Close</span>
</button>
));
ModalClose.displayName = "ModalClose";
export { Modal, ModalHeader, ModalTitle, ModalDescription, ModalContent, ModalFooter, ModalClose };