A modal is a card on a dimmed overlay that stops the page behind it: the background is inert to the pointer, to the keyboard and to assistive technology until the modal closes. That is the whole reason to reach for one, and the whole reason not to.
Anatomy
Section titled “Anatomy”- Overlay the dimmed layer over the rest of the page. It is what makes the page behind inert, and it only closes the modal when
isDismissableis on. - Container the elevated card the modal is built from. The size prop caps its width, not its height.
- Header the top bar, ruled off from the body. It holds the title and the close button, and the tab bar when one is supplied.
- Title (optional) names the task in a few words. Omit it and the header keeps only the way out.
- Close button (optional) the icon button at the top right, on by default and the only exit a modal has out of the box besides Esc.
contentProps.showCloseButtonremoves it. - Body the content area, and the only part that scrolls. Content long enough to need that scroll probably belongs on a page.
- Footer (optional) the row of actions at the bottom, arranged for you. One primary, the rest secondary.
When to use
Section titled “When to use”Use a modal when the reader has to act or answer before the flow behind them can continue — confirming something destructive, or supplying the one thing a process is missing. Stopping the page is a claim that nothing else on it matters yet, and if that claim is false the modal is an obstacle.
One at a time. Nesting modals, or opening a second from inside the first, stops a page that is already stopped, and the reader loses track of which Esc closes what.
For a surface that leaves the page usable, see Popover, which sets out the boundary between the three surfaces that open over a page. For content that belongs beside the flow rather than over it, use an inline Card.
Dismissal
Section titled “Dismissal”A modal has three exits with three different defaults
Section titled “A modal has three exits with three different defaults”Each has its own switch, and they are not symmetrical:
- The close button is on by default, and off through
contentProps={{ showCloseButton: false }}. - Esc closes by default, and stops through
isKeyboardDismissDisabled. - Clicking the overlay does nothing by default.
isDismissableis what turns it on.
So a modal out of the box closes on Esc and on the X, and ignores the overlay —
which is worth knowing, because the overlay looks like a dismissal target
whether or not it is one. Turn isDismissable on for anything the reader can
abandon safely, and leave it off where a stray click would discard work.
Set all three against you and the modal has no exit at all. Nothing in the component prevents that combination, so it is yours to avoid: keep the close button unless the footer supplies a way out, and then keep the footer.
Opening from anything but the trigger prop loses focus on close
Section titled “Opening from anything but the trigger prop loses focus on close”trigger takes the element that opens the modal and merges the open and close
behavior into it. It is also how focus finds its way home — React Aria returns
focus to the trigger it was given, so a modal opened from a button that is not
the trigger prop leaves focus wherever the page put it after the modal
unmounts.
The alternative is the controlled trio, open, defaultOpen and
onOpenChange, for the modal that opens in response to something other than a
click.
Layout
Section titled “Layout”Fit the task on one screen
Section titled “Fit the task on one screen”size is md, lg, xl, full or auto, and every one of them caps width
only. Height comes from the content, and the body is the part that scrolls.
A modal the reader has to scroll has stopped the page for something the page could have held. The same goes for a flow with more than one step: the modal gives you no back, no address and no way to leave and return, so a second step belongs on a route.
Tabs are the one way a modal holds two views
Section titled “Tabs are the one way a modal holds two views”tabs renders a Tab Bar in the header and overrides
children — pass both and the children are discarded silently. It is the one
way a modal holds more than one view, and it is a narrow one: tabs work when the
views are alternatives the reader chooses between, and not when they are steps
in an order.
noBodyPadding removes the body’s padding for content that supplies its own,
such as a full-bleed table.
Accessibility
Section titled “Accessibility”Do not hand-roll the focus trap
Section titled “Do not hand-roll the focus trap”Three things happen on open, and all three are React Aria’s rather than yours.
Focus is contained inside the card, so Tab cycles within it and cannot reach the
page behind. Everything outside the modal is hidden from assistive technology
with aria-hidden, so a screen reader cannot read past the dialog. The document
stops scrolling.
That is useModalOverlay calling useOverlayFocusContain, ariaHideOutside
and usePreventScroll, and it is why a modal needs no focus management code
around it. A hand-written trap on top competes with the one already running.
React Aria takes the accessible name from the title
Section titled “React Aria takes the accessible name from the title”title renders the header’s heading as a React Aria Heading with
slot="title", which is exactly the hook React Aria’s Dialog looks for when it
wires up aria-labelledby. So a modal with a title is named, and the
association is not something to set by hand.
A modal without one is unnamed, and React Aria says so: it logs a console warning
telling you it needs an aria-label or aria-labelledby. Pass one through
contentProps when the modal has no visible heading.
Use alertdialog for the modal the reader did not open
Section titled “Use alertdialog for the modal the reader did not open”role reaches the dialog through contentProps, and it takes dialog or
alertdialog. alertdialog is for the interruption, a session expiring or a
save failing, and it asks screen readers to announce the modal’s contents
immediately rather than its name alone. dialog, the default, is for the modal
the reader asked for.
Content
Section titled “Content”The title names the task, not the kind of dialog
Section titled “The title names the task, not the kind of dialog”“Delete account” is a title. “Confirm” is a category, and the reader has to read the body to find out which confirmation they are looking at — which is also what a screen reader announces first, since the title is the modal’s accessible name.
Put the consequence in the body’s first sentence rather than at the end of it. A modal is read in the order it is laid out, and the reader’s hand is already on the footer.
Label footer buttons with the answer
Section titled “Label footer buttons with the answer”footer takes Buttons directly and arranges them in a row. One of them is the
primary and the rest are secondary, the same emphasis rule as on
Button, and they are labeled with the answer rather than
with agreement: “Delete file” and “Keep file” tell the reader what each does
without reading the question again. “Yes” and “No” do not.
Capitalization, punctuation and the rules that apply to every label are on Writing.
| Name | Type |
|---|---|
title |
Title of the Card |
trigger |
Component to use as trigger. React Aria trigger props will be merged with this element. |
contentProps |
Props to apply to Modal.Card within |
size |
Max width of the modal |
footer |
Footer section intended for actions. You can pass in Buttons directly and they will render in a HStack. |
tabs |
Renders tabs within the modal. Adds a tab bar and will render the contents of the selected tab in a tabpanel. Overrides |
children |
Children content |
noBodyPadding |
When true, removes horizontal and vertical padding from the body. Defaults to false. Can also be set via contentProps.noBodyPadding. |
open |
Controls whether the modal is open (controlled) |
defaultOpen |
Default open state (uncontrolled) |
onOpenChange |
Handler called when open state changes |
isDismissable |
Whether the modal can be dismissed by clicking outside or pressing escape |
isKeyboardDismissDisabled |
Whether pressing the escape key closes the modal |
Was this page helpful?