Modal
Our Modal component has some important features you won't find in a standard component library.
- It uses the native
<dialog>element. - By default, it is always mounted into the DOM - even when the modal is closed. This means the dialog content won't be hidden from Google and will contribute towards SEO.
export const ModalExample = () => {
return (
<ModalRoot>
<ModalTrigger asChild>
<Button>Open Modal</Button>
</ModalTrigger>
<Modal>
<ModalBackdrop />
<ModalCloseButton />
<ModalContent>
<ModalTitle>This is a modal</ModalTitle>
<ModalDescription>
It should probably say something useful here... but this is just an
example. You can close this modal by hitting the escape key, or by
clicking the close button in the top right corner.
</ModalDescription>
<ModalFooter>
<ModalAction onClick={() => console.log('cancel')}>
<Button variant="secondary">Cancel</Button>
</ModalAction>
<ModalAction onClick={() => console.log('save')}>
<Button>Save</Button>
</ModalAction>
</ModalFooter>
</ModalContent>
</Modal>
</ModalRoot>
);
};Controlled
You can pass an isOpen prop to the <ModalRoot> to control the modal's open state
from the outside. An example use case for this is when you want to open and
close the modal based on some event other than a button click.
export const ModalExampleControlled = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<ModalRoot isOpen={isOpen} onClose={() => setIsOpen(false)}>
<ModalTrigger asChild>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
</ModalTrigger>
<Modal>
<ModalBackdrop />
<ModalContent>
<ModalTitle>Controlled Modal</ModalTitle>
<ModalDescription className="mb-4">
You can open this modal however you want.
</ModalDescription>
<ModalFooter>
<Button onClick={() => setIsOpen(false)}>Close Modal</Button>
</ModalFooter>
</ModalContent>
</Modal>
</ModalRoot>
);
};Async Actions
Modal Action buttons can take async functions as their onClick prop. While
pending the action child receives an isLoading prop, and when the callback resolves the
modal closes.
<ModalAction
onClick={async () => {
await login();
}}
>
<Button>Login</Button>
</ModalAction>To prevent the modal from closing you can call event.preventDefault() in the
onClick callback. You might want to do this in the case that the callback fails,
for example, when making an API call.
<ModalAction
onClick={async (event) => {
try {
await login();
} catch (error) {
event.preventDefault(); // Prevent the modal from closing
setError(error.message); // Handle the error
}
}}
>
<Button>Login</Button>
</ModalAction>Dismiss on backdrop click
If you want you can make the modal dismissable by clicking the backdrop you can
pass the dismissOnClick prop to the <ModalBackdrop>.
<Modal>
<ModalBackdrop dismissOnClick />
<ModalContent>...</ModalContent>
</Modal>Portals
You can also pass a container prop to the Modal component to render the modal
in a different container. This might be helpful if you want to open a modal from
inside a container that has overflow: hidden;.
<Modal container={container}>
<ModalBackdrop />
<ModalContent>...</ModalContent>
</Modal>Unmount when closed
You can choose to unmount the modal when it is closed by passing the
unmountWhenClosed prop to the Modal Root component.
<ModalRoot unmountWhenClosed>
<ModalTrigger asChild>
<Button>Open Modal</Button>
</ModalTrigger>
<Modal>
<ModalBackdrop />
<ModalCloseButton />
<ModalContent>
<ModalTitle>This is a modal</ModalTitle>
</ModalContent>
</Modal>
</ModalRoot>Requirements
The following libraries need to be installed:
| 3rd Party Libraries | Version | Install command |
|---|---|---|
@radix-ui/react-slot | ^1.2.4 | npm install @radix-ui/react-slot@^1.2.4 |
The following files and folders need to be copied to your project:
| Copy to project | Reason |
|---|---|
ui/components/modal/ | Modal components and hooks |
ui/primatives/dialog/ | The modal uses the dialog primitive |
ui/hooks/use-scroll-lock.ts | Used to prevent scroll on window when modal is open |
ui/hooks/use-keydown.ts | Used to listen for esc key to close modal |
Components
<ModalRoot />
Required at the top level. Provides context and manages open/close state for all descendant Modal components.
Can be used uncontrolled (open state managed internally via <ModalTrigger />) or controlled by passing isOpen and onClose.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The modal tree. |
id | string | — | Id for the underlying dialog element. A unique id is generated automatically if not provided. |
isOpen | boolean | — | Controls the open state. When provided, the modal becomes controlled. |
onClose | () => void | — | Called when the modal requests to close (e.g. via the Escape key or a close button). |
unmountWhenClosed | boolean | false | When true, the modal is removed from the DOM when closed. |
allowBodyScroll | boolean | false | When true, the page body remains scrollable while the modal is open. |
<ModalTrigger />
A button that opens the modal when clicked. Use asChild to render as a custom element such as <Button>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| @extends | ComponentProps<'button'> | All props from the <button> element are accepted. | |
asChild | boolean | false | When true, renders as its child element instead of <button>. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<Modal />
The native <dialog> element. Must be placed inside <ModalRoot />.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| @extends | ComponentProps<'dialog'> | All props from the <dialog> element are accepted. | |
isModal | boolean | true | When true, uses showModal() which traps focus and blocks interaction with the rest of the page. |
container | HTMLElement | — | When provided, the dialog is rendered into this container via a portal. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<ModalBackdrop />
A full-screen overlay rendered behind the modal content. Applies a semi-transparent blurred backdrop.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| @extends | ComponentProps<'div'> | All props from the <div> element are accepted. | |
dismissOnClick | boolean | false | When true, clicking the backdrop closes the modal. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<ModalContent />
A container for the modal body. Handles entry and exit animations.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'div'> | All props from the <div> element are accepted. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<ModalCloseButton />
A pre-styled circular close button positioned in the top-right corner of the modal. Receives focus automatically when the modal opens.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| @extends | ComponentProps<'button'> | All props from the <button> element are accepted. | |
onClick | (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void> | — | Called when the button is clicked, before the modal closes. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<ModalAction />
Wraps an action element (typically a <Button>) that closes the modal when clicked. Supports async onClick handlers — while the handler is pending, the child receives a loading prop. When the handler resolves the modal closes. Call event.preventDefault() to prevent the modal from closing (e.g. after a failed API call).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The action element to render (e.g. <Button>). Rendered via asChild — the child receives a loading prop while an async onClick is pending. |
onClick | (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void> | — | Called when the action is clicked. If it returns a Promise, the modal waits for it to resolve before closing. Call event.preventDefault() to cancel closing. |
Data attributes
| Data attribute | Type | Description |
|---|---|---|
data-state | open | closed | Whether the modal is open. |
<ModalTitle />
The modal title rendered as an <h2>. When present, automatically links to the dialog via aria-labelledby.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'h2'> | All props from the <h2> element are accepted. |
<ModalDescription />
The modal description rendered as a <p>. When present, automatically links to the dialog via aria-describedby.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'p'> | All props from the <p> element are accepted. |
<ModalFooter />
A flex row container for action buttons, aligned to the right.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'div'> | All props from the <div> element are accepted. |
Hooks
useDialogContext()
Call from inside <ModalRoot /> to access and control the modal state. Useful for building custom controls that need to open or close the modal programmatically.
Returns
| Property | Type | Description |
|---|---|---|
isOpen | boolean | Whether the modal is currently open. |
open | () => void | Opens the modal. |
close | () => void | Closes the modal. |
dataState | 'open' | 'closed' | The current animation state. |