Modal
Modal is a dialog overlay component for focused interactions that require user attention.
Open in Storybook
Import
import { Modal, ModalHeader, ModalBody, ModalFooter } from "@tosui/react";
Basic usage
Modal is a controlled component that requires isOpen and onClose props.
- Preview
- Code
function BasicModal() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
aria-labelledby="basic-modal-title"
>
<ModalHeader>
<Heading id="basic-modal-title" size="lg">Modal Title</Heading>
</ModalHeader>
<ModalBody>
This is the modal content.
</ModalBody>
<ModalFooter>
<Button variant="ghost" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={() => setIsOpen(false)}>Confirm</Button>
</ModalFooter>
</Modal>
</>
);
}
Sizes
<VStack gap={2} align="start">
<Button onClick={() => openModal("sm")}>Small (400px)</Button>
<Button onClick={() => openModal("md")}>Medium (500px)</Button>
<Button onClick={() => openModal("lg")}>Large (700px)</Button>
<Button onClick={() => openModal("xl")}>Extra Large (900px)</Button>
<Button onClick={() => openModal("full")}>Full Screen</Button>
</VStack>
<Modal
isOpen={isOpen}
onClose={onClose}
size={size}
aria-label={`${size} modal`}
>
<ModalBody>Content for {size} modal</ModalBody>
</Modal>
Size dimensions
| Size | Width |
|---|---|
| sm | 400px |
| md | 500px |
| lg | 700px |
| xl | 900px |
| full | 100% |
Close behaviors
Control how the modal can be closed.
// Disable close on overlay click
<Modal
isOpen={isOpen}
onClose={onClose}
closeOnOverlayClick={false}
aria-label="Example modal"
>
...
</Modal>
// Disable close on Escape key
<Modal
isOpen={isOpen}
onClose={onClose}
closeOnEsc={false}
aria-label="Example modal"
>
...
</Modal>
Common patterns
Confirmation dialog
<Modal isOpen={isOpen} onClose={onClose} size="sm" aria-label="Delete item">
<ModalHeader>
<Heading size="lg">Delete Item?</Heading>
</ModalHeader>
<ModalBody>
<Text>This action cannot be undone. Are you sure you want to delete this item?</Text>
</ModalBody>
<ModalFooter>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button colorScheme="error" onClick={handleDelete}>Delete</Button>
</ModalFooter>
</Modal>
Form modal
<Modal isOpen={isOpen} onClose={onClose} aria-label="Edit profile">
<ModalHeader>
<Heading size="lg">Edit Profile</Heading>
</ModalHeader>
<ModalBody>
<VStack gap={4}>
<FormField label="Name">
<Input value={name} onChange={(e) => setName(e.target.value)} />
</FormField>
<FormField label="Email">
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</FormField>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button onClick={handleSave}>Save Changes</Button>
</ModalFooter>
</Modal>
Long content
Modal scrolls internally when content exceeds viewport height.
<Modal isOpen={isOpen} onClose={onClose} aria-label="Terms of service">
<ModalHeader>
<Heading size="lg">Terms of Service</Heading>
</ModalHeader>
<ModalBody>
{/* Long scrollable content */}
<Text>Very long content goes here...</Text>
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>I Accept</Button>
</ModalFooter>
</Modal>
Props reference
Modal
| Prop | Type | Default | Description |
|---|---|---|---|
| isOpen | boolean | - | Whether modal is open (required) |
| onClose | () => void | - | Callback when modal should close (required) |
| size | "sm" | "md" | "lg" | "xl" | "full" | "md" | Modal size |
| closeOnOverlayClick | boolean | true | Close when clicking backdrop |
| closeOnEsc | boolean | true | Close on Escape key |
| className | string | - | Additional CSS class on the overlay |
| aria-label | string | - | Accessible name for the dialog |
| aria-labelledby | string | - | ID of an element that labels the dialog |
| children | ReactNode | - | Modal sections or content |
ModalHeader, ModalBody, ModalFooter
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Additional CSS class |
| children | ReactNode | - | Section content |
Accessibility
- The overlay uses
role="dialog"andaria-modal="true". - Give every modal an accessible name. Use
aria-labelledbywhen a visible heading labels the dialog. Usearia-labelwhen no visible heading is available. - Focus moves into the modal, stays inside it, and returns to the previously focused element when the modal closes.
- Body scrolling stops while the modal is open.
- Escape closes the modal by default.
TypeScript
import { Modal, ModalHeader, ModalBody, ModalFooter, type ModalSize, type ModalProps, type ModalHeaderProps, type ModalBodyProps, type ModalFooterProps } from "@tosui/react";