useModalOverlay

Provides the behavior and accessibility implementation for a modal component. A modal is an overlay element which blocks interaction with elements outside it.

installyarn add react-aria
version3.29.0
usageimport {useModalOverlay} from 'react-aria'

API#


useModalOverlay( props: AriaModalOverlayProps, state: OverlayTriggerState, ref: RefObject<HTMLElement> ): ModalOverlayAria

Features#


The HTML <dialog> element can be used to build modal overlays. However, it is not yet widely supported across browsers, and can be difficult to style and customize. useModalOverlay, helps achieve accessible modal overlays that can be styled as needed.

  • Accessible – Content outside the modal is hidden from assistive technologies while it is open. The modal optionally closes when interacting outside, or pressing the Escape key.
  • Focus management – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside.
  • Scroll locking – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers.

Note: useModalOverlay only handles the overlay itself. It should be combined with useDialog to create fully accessible modal dialogs. Other overlays such as menus may also be placed in a modal overlay.

Anatomy#


UnderlayModal

A modal overlay consists of an overlay container element, and an underlay. The overlay may contain a dialog, or another element such as a menu or listbox when used within a component such as a select or combobox. The underlay is typically a partially transparent element that covers the rest of the screen behind the overlay, and prevents the user from interacting with the elements behind it.

useModalOverlay returns props that you should spread onto the overlay and underlay elements:

NameTypeDescription
modalPropsDOMAttributesProps for the modal element.
underlayPropsDOMAttributesProps for the underlay element.

State is managed by the useOverlayTriggerState hook in @react-stately/overlays. The state object should be passed as an argument to useModalOverlay.

Example#


This example shows how to build a typical modal dialog, by combining useModalOverlay with useDialog. The Dialog component used in this example can also be reused within a popover or other types of overlays.

The Modal component uses an <Overlay> to render its contents in a React Portal at the end of the document body, which ensures it is not clipped by other elements. It also acts as a focus scope, containing focus within the modal and restoring it to the trigger when it unmounts. useModalOverlay handles preventing page scrolling while the modal is open, hiding content outside the modal from screen readers, and optionally closing it when the user interacts outside or presses the Escape key.

import {Overlay, useModalOverlay} from 'react-aria';

function Modal({ state, children, ...props }) {
  let ref = React.useRef(null);
  let { modalProps, underlayProps } = useModalOverlay(props, state, ref);

  return (
    <Overlay>
      <div
        style={{
          position: 'fixed',
          zIndex: 100,
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          background: 'rgba(0, 0, 0, 0.5)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }}
        {...underlayProps}
      >
        <div
          {...modalProps}
          ref={ref}
          style={{
            background: 'var(--page-background)',
            border: '1px solid gray'
          }}
        >
          {children}
        </div>
      </div>
    </Overlay>
  );
}
import {Overlay, useModalOverlay} from 'react-aria';

function Modal({ state, children, ...props }) {
  let ref = React.useRef(null);
  let { modalProps, underlayProps } = useModalOverlay(
    props,
    state,
    ref
  );

  return (
    <Overlay>
      <div
        style={{
          position: 'fixed',
          zIndex: 100,
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          background: 'rgba(0, 0, 0, 0.5)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }}
        {...underlayProps}
      >
        <div
          {...modalProps}
          ref={ref}
          style={{
            background: 'var(--page-background)',
            border: '1px solid gray'
          }}
        >
          {children}
        </div>
      </div>
    </Overlay>
  );
}
import {
  Overlay,
  useModalOverlay
} from 'react-aria';

function Modal(
  {
    state,
    children,
    ...props
  }
) {
  let ref = React.useRef(
    null
  );
  let {
    modalProps,
    underlayProps
  } = useModalOverlay(
    props,
    state,
    ref
  );

  return (
    <Overlay>
      <div
        style={{
          position:
            'fixed',
          zIndex: 100,
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          background:
            'rgba(0, 0, 0, 0.5)',
          display:
            'flex',
          alignItems:
            'center',
          justifyContent:
            'center'
        }}
        {...underlayProps}
      >
        <div
          {...modalProps}
          ref={ref}
          style={{
            background:
              'var(--page-background)',
            border:
              '1px solid gray'
          }}
        >
          {children}
        </div>
      </div>
    </Overlay>
  );
}

The below ModalTrigger component uses the useOverlayTrigger hook to show the modal when a button is pressed. It accepts a function as children, which is called with a callback that closes the modal. This can be used to implement a close button.

import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function ModalTrigger({ label, children, ...props }) {
  let state = useOverlayTriggerState(props);
  let { triggerProps, overlayProps } = useOverlayTrigger(
    { type: 'dialog' },
    state
  );

  return (
    <>
      <Button {...triggerProps}>Open Dialog</Button>
      {state.isOpen &&
        (
          <Modal {...props} state={state}>
            {React.cloneElement(children(state.close), overlayProps)}
          </Modal>
        )}
    </>
  );
}
import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function ModalTrigger({ label, children, ...props }) {
  let state = useOverlayTriggerState(props);
  let { triggerProps, overlayProps } = useOverlayTrigger({
    type: 'dialog'
  }, state);

  return (
    <>
      <Button {...triggerProps}>Open Dialog</Button>
      {state.isOpen &&
        (
          <Modal {...props} state={state}>
            {React.cloneElement(
              children(state.close),
              overlayProps
            )}
          </Modal>
        )}
    </>
  );
}
import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function ModalTrigger(
  {
    label,
    children,
    ...props
  }
) {
  let state =
    useOverlayTriggerState(
      props
    );
  let {
    triggerProps,
    overlayProps
  } = useOverlayTrigger({
    type: 'dialog'
  }, state);

  return (
    <>
      <Button
        {...triggerProps}
      >
        Open Dialog
      </Button>
      {state.isOpen &&
        (
          <Modal
            {...props}
            state={state}
          >
            {React
              .cloneElement(
                children(
                  state
                    .close
                ),
                overlayProps
              )}
          </Modal>
        )}
    </>
  );
}

Now, we can render an example modal containing a dialog, with a button that closes it using the function provided by ModalTrigger.

// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';

<ModalTrigger label="Open Dialog">
  {close =>
    <Dialog title="Enter your name">
      <form style={{display: 'flex', flexDirection: 'column'}}>
        <label htmlFor="first-name">First Name:</label>
        <input id="first-name" />
        <label htmlFor="last-name">Last Name:</label>
        <input id="last-name" />
        <Button
          onPress={close}
          style={{marginTop: 10}}>
          Submit
        </Button>
      </form>
    </Dialog>
  }
</ModalTrigger>
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';

<ModalTrigger label="Open Dialog">
  {(close) => (
    <Dialog title="Enter your name">
      <form
        style={{
          display: 'flex',
          flexDirection: 'column'
        }}
      >
        <label htmlFor="first-name">First Name:</label>
        <input id="first-name" />
        <label htmlFor="last-name">Last Name:</label>
        <input id="last-name" />
        <Button
          onPress={close}
          style={{ marginTop: 10 }}
        >
          Submit
        </Button>
      </form>
    </Dialog>
  )}
</ModalTrigger>
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';

<ModalTrigger label="Open Dialog">
  {(close) => (
    <Dialog title="Enter your name">
      <form
        style={{
          display:
            'flex',
          flexDirection:
            'column'
        }}
      >
        <label htmlFor="first-name">
          First Name:
        </label>
        <input id="first-name" />
        <label htmlFor="last-name">
          Last Name:
        </label>
        <input id="last-name" />
        <Button
          onPress={close}
          style={{
            marginTop:
              10
          }}
        >
          Submit
        </Button>
      </form>
    </Dialog>
  )}
</ModalTrigger>

Dialog#

The Dialog component is rendered within the ModalOverlay component. It is built using the useDialog hook, and can also be used in other overlay containers such as popovers.

Show code
import type {AriaDialogProps} from 'react-aria';
import {useDialog} from 'react-aria';

interface DialogProps extends AriaDialogProps {
  title?: React.ReactNode;
  children: React.ReactNode;
}

function Dialog({ title, children, ...props }: DialogProps) {
  let ref = React.useRef(null);
  let { dialogProps, titleProps } = useDialog(props, ref);

  return (
    <div {...dialogProps} ref={ref} style={{ padding: 30 }}>
      {title &&
        (
          <h3 {...titleProps} style={{ marginTop: 0 }}>
            {title}
          </h3>
        )}
      {children}
    </div>
  );
}
import type {AriaDialogProps} from 'react-aria';
import {useDialog} from 'react-aria';

interface DialogProps extends AriaDialogProps {
  title?: React.ReactNode;
  children: React.ReactNode;
}

function Dialog(
  { title, children, ...props }: DialogProps
) {
  let ref = React.useRef(null);
  let { dialogProps, titleProps } = useDialog(props, ref);

  return (
    <div {...dialogProps} ref={ref} style={{ padding: 30 }}>
      {title &&
        (
          <h3 {...titleProps} style={{ marginTop: 0 }}>
            {title}
          </h3>
        )}
      {children}
    </div>
  );
}
import type {AriaDialogProps} from 'react-aria';
import {useDialog} from 'react-aria';

interface DialogProps
  extends
    AriaDialogProps {
  title?:
    React.ReactNode;
  children:
    React.ReactNode;
}

function Dialog(
  {
    title,
    children,
    ...props
  }: DialogProps
) {
  let ref = React.useRef(
    null
  );
  let {
    dialogProps,
    titleProps
  } = useDialog(
    props,
    ref
  );

  return (
    <div
      {...dialogProps}
      ref={ref}
      style={{
        padding: 30
      }}
    >
      {title &&
        (
          <h3
            {...titleProps}
            style={{
              marginTop:
                0
            }}
          >
            {title}
          </h3>
        )}
      {children}
    </div>
  );
}

Button#

The Button component is used in the above example to toggle the popover. It is built using the useButton hook, and can be shared with many other components.

Show code
import {useButton} from 'react-aria';

function Button(props) {
  let ref = props.buttonRef;
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref} style={props.style}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = props.buttonRef;
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref} style={props.style}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref =
    props.buttonRef;
  let { buttonProps } =
    useButton(
      props,
      ref
    );
  return (
    <button
      {...buttonProps}
      ref={ref}
      style={props.style}
    >
      {props.children}
    </button>
  );
}

Usage#


The following examples show how to use the Modal and ModalTrigger components created in the above example.

Dismissable#

If your modal doesn't require the user to make a confirmation, you can set isDismissable on the Modal. This allows the user to click outside to close the dialog.

<ModalTrigger isDismissable label="Open Dialog">
  {() =>
    <Dialog title="Notice">
      Click outside to close this dialog.
    </Dialog>
  }
</ModalTrigger>
<ModalTrigger isDismissable label="Open Dialog">
  {() =>
    <Dialog title="Notice">
      Click outside to close this dialog.
    </Dialog>
  }
</ModalTrigger>
<ModalTrigger
  isDismissable
  label="Open Dialog"
>
  {() => (
    <Dialog title="Notice">
      Click outside to
      close this
      dialog.
    </Dialog>
  )}
</ModalTrigger>

Keyboard dismiss disabled#

By default, modals can be closed by pressing the Escape key. This can be disabled with the isKeyboardDismissDisabled prop.

<ModalTrigger isKeyboardDismissDisabled label="Open Dialog">
  {close =>
    <Dialog title="Notice">
      <p>You must close this dialog using the button below.</p>
      <Button onPress={close}>Close</Button>
    </Dialog>
  }
</ModalTrigger>
<ModalTrigger
  isKeyboardDismissDisabled
  label="Open Dialog"
>
  {(close) => (
    <Dialog title="Notice">
      <p>
        You must close this dialog using the button below.
      </p>
      <Button onPress={close}>Close</Button>
    </Dialog>
  )}
</ModalTrigger>
<ModalTrigger
  isKeyboardDismissDisabled
  label="Open Dialog"
>
  {(close) => (
    <Dialog title="Notice">
      <p>
        You must close
        this dialog
        using the
        button below.
      </p>
      <Button
        onPress={close}
      >
        Close
      </Button>
    </Dialog>
  )}
</ModalTrigger>