useModalOverlay
Provides the behavior and accessibility implementation for a modal component. A modal is an overlay element which blocks interaction with elements outside it.
install | yarn add react-aria |
---|---|
version | 3.33.1 |
usage | import {useModalOverlay} from 'react-aria' |
API#
useModalOverlay(
props: AriaModalOverlayProps,
state: OverlayTriggerState,
ref: RefObject<HTMLElement
| | null>
): 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#
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:
Name | Type | Description |
modalProps | DOMAttributes | Props for the modal element. |
underlayProps | DOMAttributes | Props 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>