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.

This is a modal

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.

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.

Controlled Modal

To close this modal you can hit escape but you can also type the word close into the input.

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.

This modal does async things

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.

<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>.

This modal is dismissable

Click anywhere outside the modal... go on... I dare you.

<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;.

This modal is a direct child of the document body

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 LibrariesVersionInstall command
@radix-ui/react-slot^1.2.4npm install @radix-ui/react-slot@^1.2.4

The following files and folders need to be copied to your project:

Copy to projectReason
ui/components/modal/Modal components and hooks
ui/primatives/dialog/The modal uses the dialog primitive
ui/hooks/use-scroll-lock.tsUsed to prevent scroll on window when modal is open
ui/hooks/use-keydown.tsUsed 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
PropTypeDefaultDescription
childrenReact.ReactNodeThe modal tree.
idstringId for the underlying dialog element. A unique id is generated automatically if not provided.
isOpenbooleanControls the open state. When provided, the modal becomes controlled.
onClose() => voidCalled when the modal requests to close (e.g. via the Escape key or a close button).
unmountWhenClosedbooleanfalseWhen true, the modal is removed from the DOM when closed.
allowBodyScrollbooleanfalseWhen 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
PropTypeDefaultDescription
@extendsComponentProps<'button'>All props from the <button> element are accepted.
asChildbooleanfalseWhen true, renders as its child element instead of <button>.
Data attributes
Data attributeTypeDescription
data-stateopen | closedWhether the modal is open.

The native <dialog> element. Must be placed inside <ModalRoot />.

Props
PropTypeDefaultDescription
@extendsComponentProps<'dialog'>All props from the <dialog> element are accepted.
isModalbooleantrueWhen true, uses showModal() which traps focus and blocks interaction with the rest of the page.
containerHTMLElementWhen provided, the dialog is rendered into this container via a portal.
Data attributes
Data attributeTypeDescription
data-stateopen | closedWhether the modal is open.

<ModalBackdrop />

A full-screen overlay rendered behind the modal content. Applies a semi-transparent blurred backdrop.

Props
PropTypeDefaultDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.
dismissOnClickbooleanfalseWhen true, clicking the backdrop closes the modal.
Data attributes
Data attributeTypeDescription
data-stateopen | closedWhether the modal is open.

<ModalContent />

A container for the modal body. Handles entry and exit animations.

Props
PropTypeDescription
@extendsComponentProps<'div'>All props from the <div> element are accepted.
Data attributes
Data attributeTypeDescription
data-stateopen | closedWhether 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
PropTypeDefaultDescription
@extendsComponentProps<'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 attributeTypeDescription
data-stateopen | closedWhether 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
PropTypeDefaultDescription
childrenReact.ReactNodeThe 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 attributeTypeDescription
data-stateopen | closedWhether the modal is open.

<ModalTitle />

The modal title rendered as an <h2>. When present, automatically links to the dialog via aria-labelledby.

Props
PropTypeDescription
@extendsComponentProps<'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
PropTypeDescription
@extendsComponentProps<'p'>All props from the <p> element are accepted.

<ModalFooter />

A flex row container for action buttons, aligned to the right.

Props
PropTypeDescription
@extendsComponentProps<'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
PropertyTypeDescription
isOpenbooleanWhether the modal is currently open.
open() => voidOpens the modal.
close() => voidCloses the modal.
dataState'open' | 'closed'The current animation state.