A dialog is a floating element that displays information that requires immediate attention, appearing over the page content and blocking interactions with the page until it is dismissed.
It has similar interactions to a popover but with two key differences:
- It is modal and renders a backdrop behind the dialog that dims the content behind it, making the rest of the page inaccessible.
- It is centered in the viewport, not anchored to any particular reference element.
Essentials
An accessible dialog component has the following qualities:
- Dismissal: When the user presses the
esc
key or outside the dialog while it is open, it closes. - Role: The elements are given relevant role and ARIA attributes to be accessible to screen readers.
- Focus management: Focus is fully trapped inside the dialog and must be dismissed by the user.
Examples
Both of these examples have sections explaining them in-depth below.
Basic dialog
This example demonstrates how to create a dialog for use in a single instance to familiarize yourself with the fundamentals.
Let’s walk through the example:
Open state
isOpen
determines whether or not the dialog is
currently open on the screen. It is used for conditional
rendering.
useFloating Hook
The useFloating()
Hook provides context for our dialog. We
need to pass it some information:
open
: The open state from ouruseState()
Hook above.onOpenChange
: A callback function that will be called when the dialog is opened or closed. We’ll use this to update ourisOpen
state.
Interaction Hooks
Interaction Hooks return objects containing keys of props that enable the dialog to be opened, closed, or accessible to screen readers.
Using the context
that was returned from the Hook,
call the interaction Hooks:
useClick()
adds the ability to toggle the dialog open or closed when the reference element is clicked. A dialog may not be attached to a reference element though, so this is optional.useDismiss()
adds the ability to dismiss the dialog when the user presses theesc
key or presses outside of the dialog. TheoutsidePressEvent
option is set to'mousedown'
so that touch events become lazy and do not fall through the backdrop, as the default behavior is eager.useRole()
adds the correct ARIA attributes for adialog
to the dialog and reference elements.
Finally, useInteractions()
merges all of their props into
prop getters which can be used for rendering.
After this:
useId()
generates a unique id for the heading and description elements of the dialog, so that the content of the dialog is announced by screen readers with wide compatibility.
Rendering
Now we have all the variables and Hooks set up, we can render out our elements.
{...getReferenceProps()}
/{...getFloatingProps()}
spreads the props from the interaction Hooks onto the relevant elements. They contain props likeonClick
,aria-expanded
, etc.<FloatingOverlay />
is a component that renders a backdrop overlay element behind the floating element, with the ability to lock the body scroll.FloatingOverlay
docs.<FloatingFocusManager />
is a component that manages focus of the dialog for modal behavior, trapping focus inside. It should directly wrap the floating element and only be rendered when the dialog is also rendered.FloatingFocusManager
docs.
Reusable dialog component
It is better to create a reusable component API that can be used in a variety of different scenarios more easily. We can place all of our Hooks into a single custom Hook for better reusability, which is then used by a controller component which encapsulates the state.
The reusable component can:
- Be uncontrolled or controlled
- Accept any element as the
<DialogTrigger />
- Read the open state to change styles
Controller component
<Dialog />
This is the controller component that manages the dialog’s state and provides the API to the rest of the components.
Render components
These components read the context provided by the root Dialog component and render the appropriate elements.
The components must be wrapped in forwardRef()
to allow
refs, and should merge the refs to ensure all refs are preserved
and forwarded to the element. Props are also merged to prevent
overwriting.
<DialogTrigger />
is the trigger button the dialog is attached to. This accepts anasChild
prop if you want to attach it to a custom element. It also has adata-state
attached to style based on the open/closed state.<DialogContent />
is the dialog element, which can contain any children (React nodes).<DialogHeading />
is the heading element for the dialog.<DialogDescription />
is the description element for the dialog.<DialogClose />
is the close button for the dialog.