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.
<ModalRoot>
<ModalTrigger asChild>
<Button>Open Modal</Button>
</ModalTrigger>
<Modal>
<ModalBackdrop />
<ModalContent>
<ModalCloseButton />
<ModalIcon className="mb-6">
<RiShapesLine />
</ModalIcon>
<ModalTitle>This is a modal title</ModalTitle>
<ModalText>
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.
</ModalText>
<ModalFooter>
<ModalAction>
<Button variant="secondary">Cancel</Button>
</ModalAction>
<ModalAction>
<Button>Confirm</Button>
</ModalAction>
</ModalFooter>
</ModalContent>
</Modal>
</ModalRoot>Modal icons
The <ModalIcon> has a few different variants. Each variant comes with a
default icon, but you can render any icon as a child to override the default.
Default icons
<ModalIcon variant="success" />Custom icons
<ModalIcon variant="success">
<RiShapesLine />
</ModalIcon>Controlled
To use as a controlled modal you need to:
- Pass a boolean
isOpenprop to the<ModalRoot>. - Pass an
onClosehandler to the<ModalRoot>. This is called when something inside the modal calls a close event (e.g. hitting escape). - Open the modal via an
onClickon the<ModalTrigger>, or via some other event.
Accessibility note
Be careful when using the Modal in a controlled state. The element that opens the modal should receive all the same accessibility properties that the <ModalTrigger /> receives.export const ControlledModal = () => {
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>
<ModalCloseButton />
<ModalIcon className="mb-6">
<RiShapesLine />
</ModalIcon>
<ModalTitle>This is controlled a modal</ModalTitle>
<ModalText>
This modal is controlled by a boolean prop on the ModalRoot
component.
</ModalText>
<ModalFooter>
<ModalAction>
<Button variant="secondary">Cancel</Button>
</ModalAction>
<ModalAction>
<Button>Save</Button>
</ModalAction>
</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
dangerouslyUnmountWhenClosed prop to the Modal Root component.
Accessibility warning
This prop should only be used as a last resort if you absolutely need the content removed from the DOM when the dialog is closed.
Unmounting a dialog when it's not in the DOM may cause accessibility issues. Specifically some screen readers may not re-evaluate the DOM when the modal opens, causing it to not announce the modal content.
<ModalRoot dangerouslyUnmountWhenClosed>
<ModalTrigger asChild>
<Button>Open Modal</Button>
</ModalTrigger>
<Modal>
<ModalBackdrop />
<ModalContent>
<ModalCloseButton />
<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). |
dangerouslyUnmountWhenClosed | 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 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. |
<ModalText />
A simple presentation container for text content.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'div'> | All props from the <div> element are accepted. |
<ModalIcon />
Display a pretty icon at the top of the modal.
Props
| Prop | Type | Description |
|---|---|---|
| @extends | ComponentProps<'div'> | All props from the <div> element are accepted. |
variant | 'default' | 'neutral' | 'success' | 'error' | 'warning' | 'info' | Effects the color od the icon and container. Some of the cariants also come with a default icon. |
<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. |