v0.1.6·How-To
Create a Modal Dialog
Build modal dialogs with proper focus management and keyboard interactions.
Published: 12/17/2025
Create a Modal Dialog
This guide shows how to create modal dialogs that trap focus and respond to keyboard events.
Simple modal
Use React state to control modal visibility:
import { useState } from 'react';
import { Button, Card } from 'ui-lab-components';
export function ModalExample() {
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setIsOpen(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
{isOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<Card className="w-full max-w-md">
<Card.Header>
<Card.Title>Confirm Action</Card.Title>
</Card.Header>
<Card.Content>
<p>Are you sure you want to continue?</p>
</Card.Content>
<Card.Footer className="flex gap-2">
<Button variant="secondary" onClick={handleClose}>
Cancel
</Button>
<Button onClick={() => { handleClose(); /* do action */ }}>
Confirm
</Button>
</Card.Footer>
</Card>
</div>
)}
</>
);
}
Close on escape
Add keyboard handling:
import { useEffect } from 'react';
export function ModalExample() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}
}, [isOpen]);
// rest of component...
}
Close on backdrop click
Dismiss when clicking outside the modal:
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
setIsOpen(false);
}
};
{isOpen && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={handleBackdropClick}
>
{/* Modal content */}
</div>
)}
Focus management
Keep focus inside the modal while it's open. Use useRef to trap focus:
import { useRef, useEffect } from 'react';
export function ModalExample() {
const [isOpen, setIsOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen && modalRef.current) {
const focusableElements = modalRef.current.querySelectorAll(
'button, input, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
(focusableElements[0] as HTMLElement).focus();
}
}
}, [isOpen]);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
{isOpen && (
<div
ref={modalRef}
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={(e) => {
if (e.target === e.currentTarget) setIsOpen(false);
}}
onKeyDown={(e) => {
if (e.key === 'Escape') setIsOpen(false);
}}
>
<Card className="w-full max-w-md">
{/* Modal content */}
</Card>
</div>
)}
</>
);
}
Animated modal
Add fade-in animation:
{isOpen && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center animate-fade-in"
onClick={handleBackdropClick}
>
<Card className="w-full max-w-md animate-scale-in">
{/* Modal content */}
</Card>
</div>
)}
Add these to your Tailwind config or global CSS:
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
Complete example with all features
import { useState, useRef, useEffect } from 'react';
import { Button, Card } from 'ui-lab-components';
export function AdvancedModal() {
const [isOpen, setIsOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}
}, [isOpen]);
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
setIsOpen(false);
}
};
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
{isOpen && (
<div
ref={modalRef}
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={handleBackdropClick}
>
<Card className="w-full max-w-md">
<Card.Header>
<Card.Title>Delete Item</Card.Title>
</Card.Header>
<Card.Content>
<p>This action cannot be undone.</p>
</Card.Content>
<Card.Footer className="flex gap-2">
<Button variant="secondary" onClick={() => setIsOpen(false)}>
Keep
</Button>
<Button variant="destructive" onClick={() => setIsOpen(false)}>
Delete
</Button>
</Card.Footer>
</Card>
</div>
)}
</>
);
}
Next step
Learn to validate form inputs inside modals in Build a Form with Validation.
© 2025 UI Lab • Built for humans and machines