Skip to main content

ModalDialog

Displays an overlay area at the center of the screen.

github
View source code

Migration Information

For teams migrating from the V1 to V2 component, please refer to the migration guide for changes to the component.
Component Guide
import { ModalDialog } from '@uhg-abyss/web/ui/ModalDialog';
    () => {
    const [isOpen, setIsOpen] = useState(false);
    return (
    <React.Fragment>
    <ModalDialog
    size="md"
    header="Modal dialog header"
    isOpen={isOpen}
    onClose={() => setIsOpen(false)}
    closeOnClickOutside
    >
    Press escape to close the Modal
    </ModalDialog>
    <Button
    onClick={() => setIsOpen(true)}
    aria-haspopup="dialog"
    >
    Open Modal Dialog
    </Button>
    </React.Fragment>
    );
    };

    Opening the ModalDialog​

    There are two methods of controlling the open state of a ModalDialog: by using the useOverlay hook or by using the isOpen prop.

    useOverlay​

    The useOverlay hook returns an object containing various methods used to control the state of the modal. Each ModalDialog must be assigned a unique model prop and be a descendent of an OverlayProvider.

    Tip

    If you're encountering issues, ensure your application is properly wrapped in an OverlayProvider and that all other props are being used correctly.

    useState​

    The open state of the ModalDialog can also be controlled with the isOpen prop in conjunction with React's useState hook.

    Use the header prop to set the header of the modal. This prop is optional but strongly recommended. It accepts a React node, and is typically a string representing the title of the modal.

    If not using header, please use ariaLabel to provide a title and keep the component accessible.

    Use the footer prop to add a footer to the drawer. It accepts a React node, typically one or two buttons.

    Passing data​

    External data can be passed into the modal dialog when using the useOverlay hook. The open and toggle methods returned by the hook accept an object of the following type:

    {
    isOpen?: boolean;
    data?: object;
    }

    The getState method retrieves the state of the modal dialog as an object of the same type.

    Size​

    Use the size prop to set the width of the modal dialog. There are three preset values:

    • 'sm', with a max width of 348px;
    • 'md', with a max width of 720px;
    • 'fullscreen', which takes up the full viewport width and height.

    The default value is 'md'.

    It is also possible to provide a custom size by providing a string or a number to the size prop.

    Overflow​

    Overflow is handled within the content of the modal dialog. The header and footer, if present, will remain fixed.

    Close on click outside​

    Use the closeOnClickOutside prop to control whether a user can dismiss the modal dialog by clicking on the overlay. The default value is true.

    Responsive visibility​

    Warning

    Avoid hiddenFrom and visibleFrom on ModalDialog and on the control that opens it. Neither placement behaves the way it does on a presentational component and hiding the control can cause keyboard users to lose their place on the page. Prefer deciding what to render before the dialog is reachable, for example, routing small screens to a different flow.

    On the dialog itself​

    Hiding an open dialog with CSS alone would leave it holding the scroll lock, the focus trap, and the escape-key handler while invisible. ModalDialog therefore treats the breakpoint as part of its open state: when an open dialog crosses into a hidden range it closes for real, unmounting its content and calling onClose.

    So, onClose fires on a viewport resize with nobody having interacted with the dialog. If your handler does more than set state, such as logging an abandoned flow, discarding a draft, or submitting an answer, it can run when a user resizes the window or rotates their device.

    Opening a dialog that is already in its hidden range does nothing visible and does not call onClose: the dialog never opened, so there is nothing to close. Its isOpen prop stays true, though, so the dialog appears on its own once the viewport returns to a visible range, acting on a click the user made at a width where nothing happened.

    On the control that opens it​

    Hiding the trigger rather than the dialog does not avoid the problem. In fact, it introduces a worse one. A dialog returns focus to the control that opened it, and a hidden control cannot take focus. If a keyboard user opens the dialog and the viewport then crosses the breakpoint so that control is hidden, closing the dialog drops focus onto the page body: the user loses their place and the next Tab starts again from the top of the document.

    Note

    This is not unique to these props. A control that unmounts while its overlay is open loses focus the same way.

    Alert dialog​

    Use the alertDialog prop to set the role of the ModalDialog to "alertdialog". This is useful if the contents of the dialog require immediate attention, as it will cause assistive technologies to immediately announce the dialog content. The default value is false, which results in a role of "dialog".

    ModalDialog Props​

    Name
    Type
    DefaultA11yDescription
    alertDialog
    boolean | undefined
    false
    If true, the component will use role="alertdialog" instead of the default role="dialog". This should only be used if the dialog contains an important message and requires immediate attention, as it will cause assistive technologies to immediately announce the dialog content.
    ariaLabel
    string | undefined
    Provides the dialog's accessible name for screen readers when no visible header is rendered.
    ariaLive
    "off" | "assertive" | "polite" | undefined
    Controls how assistive technologies announce updates to the dialog content.
    Useful with disableAriaDescribedBy for dynamic updates where full content re-announcement should be avoided.
    children
    string | number | bigint | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<...> | null | undefined
    The contents of the modal base
    className
    string | undefined
    CSS class name to apply to each element within the component
    closeOnClickOutside
    boolean | undefined
    true
    If true, the modal base will close when the overlay is clicked
    css
    CSSProperties | Partial<Record<"abyss-modal-base-body" | "abyss-modal-base-footer" | "abyss-modal-base-header" | "abyss-modal-base-content-container" | "abyss-modal-base-close-button" | "abyss-modal-base-close-icon" | "abyss-modal-base-overlay" | "abyss-modal-base-header-container", CSSProperties>> | undefined
    Object containing CSS styling to apply; uses the classes listed in the "Integration" tab of the component's documentation
    See CSS Prop Styling for more information
    data-testid
    string | undefined
    Suffix used to create a unique ID used for automated testing
    See Component Testing for more information
    disableAriaDescribedBy
    boolean | undefined
    false
    If true, the dialog will not set an aria-describedby value. Use this only when the dialog content is long or already fully described elsewhere.
    disableDefaultProviderProps
    boolean | undefined
    false
    If set to true, the component will not use the DefaultPropsProvider values.

    If you aren’t using the DefaultPropsProvider, this prop can be ignored.
    footer
    string | number | bigint | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<...> | null | undefined
    If present, the modal base will have a footer
    header
    string | number | bigint | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<...> | null | undefined
    The header of the modal base.
    hiddenFrom
    Breakpoint | undefined
    When defined, the component is hidden once the screen width reaches the specified breakpoint.
    hideClose
    boolean | undefined
    false
    If true, the modal base will not have a close button in the top right corner
    isOpen
    boolean | undefined
    false
    If true, the modal base is open
    model
    string | undefined
    A unique identifier for the modal base; only used when using OverlayProvider
    onClose
    (() => void) | undefined
    Callback function executed when the modal base is closed
    size
    string | number | undefined
    'md'
    The size of the modal base
    slotProps
    Partial<Record<ToSlotKey<{ overlay: LeafSlotDef<"div", undefined>; contentContainer: LeafSlotDef<"div", undefined>; headerContainer: LeafSlotDef<"div", undefined>; ... 4 more ...; footer: LeafSlotDef<...>; }, "modal-base">, SlotPropsLeafValue>> | undefined
    Props to pass to individual internal slots of the component.
    See slotProps for more information
    visibleFrom
    Breakpoint | undefined
    When defined, the component is hidden until the screen width reaches the specified breakpoint.

    ModalDialog Classes​

    Class NameDescription
    .abyss-modal-base-bodyModalBase body container
    .abyss-modal-base-close-buttonModalBase close button element
    .abyss-modal-base-close-iconModalBase close button icon element
    .abyss-modal-base-content-containerModalBase content container
    .abyss-modal-base-footerModalBase footer container
    .abyss-modal-base-headerModalBase header element
    .abyss-modal-base-header-containerModalBase header container
    .abyss-modal-base-overlayModalBase overlay element

    Adheres to the WAI-ARIA Dialog design patternopens in a new window.

    See a modal dialog exampleopens in a new window on the WAI-ARIA website.

    Keyboard Interactions​

    KeyDescription
    EscCloses the dialog.
    TabMoves focus to the next interactive element within the dialog. Once the last interactive element in the dialog is reached, pressing Tab again moves focus to the first interactive element within the dialog.
    Shift + TabMoves focus to the previous interactive element within the dialog. Once the first interactive element in the dialog is reached, pressing Shift + Tab again moves focus to the last interactive element within the dialog.

    Dialog Content​

    The content included on the dialog must be accessible.

    Triggering Elements​

    Use the aria-haspopup attribute on buttons or other triggering elements that open content like dialogs, listboxes, trees, menus, grids, etc. Use a corresponding value that indicates what kind of popup will be displayed when the trigger element is activated. In turn, the element that pops up must be of the role indicated. In the case of ModalDialog, use aria-haspopup="dialog" on the element that opens the modal dialog. ModalDialog sets role="dialog" on the dialog container internally.

    See the docs on aria-haspopupopens in a new window for more details.

    Dialog descriptions​

    ModalDialog sets aria-describedby to the dialog body by default, which works best for short dialogs that need a concise description. For long or content-heavy dialogs (where the full body would be repeated), set disableAriaDescribedBy to remove the attribute and avoid redundant announcements.

    When your dialog content updates dynamically (for example, multi-step forms or submission results), set ariaLive="polite" so updates are announced without re-announcing the full container content.

    Alert dialog​

    Known BrAT Issues​

    NVDA (Chrome, Firefox) announces dialog contents twice

    Component Tokens​

    Tip

    Click on the token row to copy the token to your clipboard.

    ModalDialog Tokens​

    Token NameValue
    modal-dialog.color.border.separator
    #CBCCCD
    modal-dialog.color.icon.active
    #000000
    modal-dialog.color.icon.hover
    #323334
    modal-dialog.color.icon.rest
    #4B4D4F
    modal-dialog.color.surface.container
    #FFFFFF
    modal-dialog.color.surface.overlay
    rgba(0,0,0,0.4)
    modal-dialog.color.text.heading
    #002677
    modal-dialog.color.text.paragraph
    #4B4D4F
    modal-dialog.border-radius.all.container
    8px
    modal-dialog.border-width.separator
    1px
    modal-dialog.sizing.all.icon
    24px
    modal-dialog.spacing.gap.horizontal.header
    8px
    modal-dialog.spacing.gap.vertical.content
    16px
    modal-dialog.spacing.padding.all.overlay.mobile
    16px
    modal-dialog.spacing.padding.bottom.content
    16px
    modal-dialog.spacing.padding.bottom.header
    12px
    modal-dialog.spacing.padding.bottom.footer
    16px
    modal-dialog.spacing.padding.horizontal.content
    16px
    modal-dialog.spacing.padding.horizontal.footer
    16px
    modal-dialog.spacing.padding.horizontal.header
    12px
    modal-dialog.spacing.padding.horizontal.overlay.web
    16px
    modal-dialog.spacing.padding.top.content
    16px
    modal-dialog.spacing.padding.top.header
    12px
    modal-dialog.spacing.padding.vertical.footer
    16px
    modal-dialog.spacing.padding.vertical.overlay.web
    48px
    Table of Contents