<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5HGSQD2L" height="0" width="0" style="display:none;visibility:hidden" title="GTM"></iframe>

Modal Dialog

A card or panel that appears over the page content, requiring user interaction before returning to the content.
scriptKeys: modalDialogstyleKeys: modalDialog

Use Modal Dialog...

  • To capture immediate attention for critical information without leaving the current context.
  • To confirm significant actions to prevent user errors.
  • To collect brief inputs in-context with minimal disruption.

Don't use Modal Dialog...

  • To show lengthy content that requires scrolling.
  • To layer over another modal.
  • To facilitate complex decisions.
  • To implement checkout flows.
  • To display SEO-critical information.

The preview has been updated.

  • The modal dialog uses the native <dialog> HTML Element. By default, the contents of the dialog are rendered in the DOM, even when closed.
  • In the React API, use the ModalDialogNav element to wrap the ModalDialogCloseButton (as well as any other elements in the dialog's "top nav", such as a "back" button on a panel).
  • You need to place localized text inside the ModalDialogCloseButton that explains what the button does, e.g. "Close". This text will not be visible on screens, but supports accessibility.
  • Any ModalDialogButtons should go inside the ModalDialogFooter and not the ModalDialogBody.
  • There is an option that makes a panel's body be "capped", giving it a maximum width. (In React, this is bodyWidth="capped".) Most panel dialogs should use this option; Gallery and PDC are notable exceptions since their panels need extra width.
  • All "panel" and "menu" dialogs should use the "pinned" option that pins their footers to the bottom of the panel. In React, this is <ModalDialogFooter pinned>.
  • If you are using the Vanilla API, you now need an empty swan-modal-dialog-nav at the top of the dialog, your dialog's main content should be wrapped in swan-modal-dialog-body, and your buttons should be wrapped in swan-modal-dialog-footer.

React

The React ModalDialog requires 2 props:

  1. isOpen- whether or not the ModalDialog should be open.
  2. onRequestDismiss- a callback function which will be invoked when the ModalDialog wants to be dismissed. This can happen in a number of different ways e.g. user clicked on the ModalDialogCloseButton, user pressed the escape key, user "clicked away".

If you are using the React API, there is a ModalDialogNav element that should wrap your ModalDialogCloseButton (as well as any other elements in the dialog's "top nav", such as a "back" button on a panel).

The preview has been updated.

Vanilla API

  • If you are using the Vanilla API, you need:
    • a wrapper swan-modal-dialog-nav at the top of the dialog, with the close button inside it using class swan-modal-dialog-close-button
    • a wrapper swan-modal-dialog-body around your dialog's main content
    • a wrapper swan-modal-dialog-footer with your buttons inside it.
  • The dialog itself is a <dialog> tag with the appropriate attributes set on it. It can sit anywhere in the HTML when the page loads.
  • The modal should be opened using the native showModal function.
Share
Share

Conditionally render contents

Use onlyRenderWhenOpen to stop the 'ModalDialogContent' being rendered in the DOM when the dialog is not yet open. This is useful if you have a network request or tracking fired inside of a 'useEffect' on mount.

The preview has been updated.

Dialog Buttons

  • Many Modal Dialogs will have a button or two at the bottom, e.g. "Cancel"/"Confirm".
  • Render these buttons inside of a ModalDialogButtons component, itself inside a ModalDialogFooter component, to ensure consistent spacing.

The preview has been updated.

You can also place labeling text to the left of a button using the ModalDialogButtonsText sub-component. This sub-component can contain any typography you want, and even a link:

The preview has been updated.

Panel

The panel option turns the dialog into a panel that slides out from the side of the screen.

  • variant="panel-right" dialogs slide out from the right side, and will be as tall as the screen. Use these to provide additional detail and/or choices.
  • variant="panel-bottom" dialogs slide up from the bottom, and will be as wide as the screen. They are used for messaging, in applications such as Studio and Cart/Checkout.
  • variant="panel-top" dialogs slide down from the top, and will be as wide as the screen. They are used for navigation content such as a hidden search form.
  • All panel and menu dialogs should use the pinned option that pins their footers to the bottom of the panel. In React, this is <ModalDialogFooter pinned>.
  • There is a option that makes a panel's body be capped, giving it a maximum width. (In React, this is bodyWidth="capped".) Most panel dialogs should use this option; Gallery and PDC are notable exceptions, since their panels need extra width.

Most panel dialogs should use the pinned footer (see below) and bodyWidth="capped" (also see below).

The preview has been updated.

Share

Panel with a custom nav

A panel dialog may wish to have a custom nav at the top of the panel, by adding additional elements to the <ModalDialogNav> element. This can even include placing the dialog's <ModalDialogHeader> inside the <ModalDialogNav>.

The preview has been updated.

  • The menu option is for left-hand flyout menus, such as the site navigation on mobile devices.
  • All panel and menu dialogs should use the pinned option that pins their footers to the bottom of the panel. In React, this is <ModalDialogFooter pinned>.

Info:

Note that even though the menu variant currently looks a lot like the "panel-left" variant, you should prefer to use the menu variant for menu-like content, in case our menu styling diverges from our panel styling in the future.

The preview has been updated.

Share
Share

panel and menu dialogs allow a ModalDialogFooter to be "pinned" to the bottom of the dialog, always staying visible as the user scrolls through the dialog content. This is the recommended layout for any panel or menu dialog with a footer.

The preview has been updated.

Share
Share

Body width

The dialog defaults to a max width of 600px (on Medium screens) or 95% of screen width (on smaller screens), but will attempt to stretch to fit its contents. Other width options include capped or grow.

Capped

Panel dialogs can set their body width to be capped at 344px, but 100% on XS screens. Most panel dialogs will want to use this option, save for those exceptions (as in Gallery or PDC) that have panels with especially wide content.

The preview has been updated.

Grow

The grow option will make the modal dialog grow as wide as the content requires, up to almost the full browser width.

The preview has been updated.

Full bleed

The fullBleed option makes the dialog's content touch the dialog's edges, by removing the padding around the dialog content.

The preview has been updated.

Share
Share

Takeover

Setting the takeOver option will cause the dialog to fill the full screen.

Info:

Note that the React component spells this takeOver with a capital "O", as if "takeover" were two words. We're sorry about this; it was an oversight.

The preview has been updated.

No close button

The "no close button" option will suppress the "X" close button in the dialog. Your dialog must provide some other visible and accessible way to close it.

Share

Background color

If you want to put a background color on a dialog, put it into the ModalDialog element.

Share

Browser History

Sometimes, we want our ModalDialog to close when the user clicks the browser's "back" button. This is especially important on Android devices where the "back" button is globally available across all apps.

On a mobile device, when a Modal Dialog opens, it may appear to the user as if they've navigated to a new page, because the dialog covers all or most of the screen. In this context, it is natural to assume that the back button will close the dialog.

Therefore, we support a Browser History feature that lets the "back" button close the dialog:

React

We provide a useBrowserHistoryState hook which is capable of syncing React state-changes with the browsers history stack.

It relies on the fact that you can actually push arbitrary (serializable) data on to the browser's history stack without changing the URL.

So, we're able to serialize and store your state as part of the history stack whenever it changes. Then, when the user interacts with the back/forward/refresh buttons, we can get the new value from the history stack and update it the local React state accordingly.

The usage/signature is very similar to React.useState. The main difference is that we require a key in order to identify your state in the browser's history stack.

Share

Use it just like you'd use React.useState and your state-changes will automatically become undo/redo-able via the back/forward buttons and the state will be persisted across refreshes as an added bonus.

Info:

Be mindful of where you use this hook. Pushing loads of entries onto the browser's history stack could result in a bad UX if the user wants to make their way back to a previous page and needs to run through all of your states as they repeatedly click the back button. 

In general, only use this hook if your state-changes feel like navigation events.

The preview has been updated.

With this code, we will see the following behavior:

  • <button> is clicked, setIsOpen(true), browser history entry is created with { 'modal-dialog-open': true }.
  • back button is clicked, browser history is updated and there is no modal-dialog-open state associated with the now-current history entry, so isOpen is set to false (the default value).
  • {replace:true} be used as a third parameter in the function useBrowserHistoryState() to indicate that when the user closes a modal dialog by clicking on the close icon, the modal should not be reopened if the user goes back to the previous page.

Accessibility

  • If the dialog has an internal title, then the dialog's outermost tag should have an attribute aria-labelledby whose value is the id of the title; the React component will do this automatically if there is a ModalDialogTitle component. If the dialog does not have an element that can act as its title, then the outermost tag needs an aria-label attribute whose value acts as a title for the dialog; the text for this value must be localized, since some browsers will read it to the user.
  • If the dialog has a piece of content that can act as a description of the dialog's contents, then the outermost tag should have an attribute aria-describedby whose value is the id of that descriptor element.
  • You need to place localized text inside the ModalDialogCloseButton that indicates the function of the close button, e.g. "Close". This text will not be visible on screens, but supports accessibility.
  • The dialog will normally place focus onto the close button once the dialog opens. If you are using the "no close button" option, the dialog may attempt to put focus on the first interactive element (button, input field, etc) inside the dialog. If no such element exists at the time the dialog opens, you will need to move focus yourself onto the first interactive element, once one becomes available.

Implementation

The polyfill for older browsers will move the location of the dialog node within the DOM tree, in order to position it properly on the screen and to provide accessibility support. Therefore, you shouldn't rely upon the dialog being able to inherit any CSS from any given ancestor, and you shouldn't use an ancestor selector in your CSS that expects the dialog to have a certain ancestor. Any CSS selectors for the dialog content should only refer to elements within the dialog itself.

Developer Guidelines - React API

Unit Testing in Jest

jsdom, the node-based implementation of browser standards that jest uses to render react inside of unit tests, does not support the HTMLDialogElement API. This might cause unit tests to fail with an error like "dialogRef.current.showModal is not a function." Add the following to your jest setup file (referenced in `setupFilesAfterEnv`:

Share

Note: Since JSDOM doesn't implement the HTMLDialogElement API, the onRequestDismiss callback won't be triggered when the dialog's close event occurs.

Developer Guidelines - Vanilla API

  • The JavaScript file for the dialog should go at the end of the body tag, and not in the head tag. This will ensure that it smoothly loads its polyfill as needed.
    • All dialog elements on the page with the class name swan-dialog will be registered by the script automatically.
    • dialogs added to the DOM after the initial load can be registered by calling window.registerVanillaSwanDialog(dialogEl)
  • The dialog will be hidden by default. It should be opened by calling the showModal() function of the dialog element.
  • The dialog will close automatically when there is a click on the backdrop of the modal. It will not close when the X button is clicked - to close the modal, call the native close() function of the element. Once the dialog has been closed, it will trigger a close event.
  • The dialog does not allow you to change its options or configuration after the dialog has been instantiated.

Best practices

Content

  • Use modal dialogs for critical information that requires immediate attention and keeps the user engaged with the current task.
  • Avoid requiring the user to make complex decisions within the dialog.
  • Ensure modal dialogs are short enough to avoid the need for scrolling on small screens.
  • To manage large amounts of content within a dialog:
    • Consider re-formulating content to be more concise or splitting it across multiple steps, such as in a wizard flow.
    • Use forms of progressive disclosure, such as tabs or accordions.

Complexity and flow

  • Avoid using modal dialogs that open other modal dialogs, as this can create a confusing experience and is not supported by our modal component.
  • Avoid using modal dialogs in checkout flows, where they can interrupt and complicate the user's purchasing process.

Button configuration

Placement

  • Position the primary action button to the right of secondary actions, using primary and secondary button styling. Refer to Button guidelines for more info.
  • In dialogs with multiple actions, arrange buttons in the following order:
    • Destructive actions (e.g., delete, sign out),
    • Dismissive (e.g., cancel)
    • Affirmative actions (e.g., confirm, sign in).

SEO considerations

  • Panel dialogs must be rendered on the server, and part of the page's source HTML, to be readable by Googlebot.
  • Do not include SEO-relevant information in modal dialogs, as they are typically not accessible to search engine crawlers. Consult the Organic Search team for guidance on placing content.

Props

Dialog
This component is implemented using the <dialog /> as the root element. You can utilize the native attributes supported by <dialog /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
proptyperequireddescriptiondefault
onRequestDismiss() => voidtrue
Callback function which will be invoked when the modal wants to be dismissed.
null
variant"standard" | "menu" | "panel-right" | "panel-left" | "panel-bottom" | "panel-top" | nullfalse
Specify Modal type
standard
isOpenboolean | nullfalse
Show/hide dialog
null
takeOverboolean | nullfalse
Decide whether to fill the full screen.
null
bodyWidth"standard" | "capped" | "grow" | nullfalse
Width of the dialog body. Some dialog variants allow the body width to be limited (capped)
null
onlyRenderWhenOpenboolean | nullfalse
only renders the contents of the dialog if the modal is open this is different than native dialog behavior, where the content is rendered but hidden
false
ModalDialogBody
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialogButtonsText
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialogButtons
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialogCloseButton
This component is implemented using the <button /> as the root element. You can utilize the native attributes supported by <button /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
proptyperequireddescriptiondefault
visuallyHiddenLabelReactNodeLikefalse
Specify the node or element that should be used for screen readers.
null
accessibleTextstring | nullfalse
Provide the text that should be used for screen readers.
null
ModalDialogContent
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
proptyperequireddescriptiondefault
fullBleedboolean | nullfalse
removes the padding around the dialog's content
null
ModalDialogFooter
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
proptyperequireddescriptiondefault
pinnedboolean | nullfalse
To pin footers to the bottom of the dialog
null
ModalDialogHeader
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialogNav
This component is implemented using the <div /> as the root element. You can utilize the native attributes supported by <div /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialogTitle
This component is implemented using the <h2 /> as the root element. You can utilize the native attributes supported by <h2 /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
ModalDialog
This component is implemented using the <dialog /> as the root element. You can utilize the native attributes supported by <dialog /> tag. The ref is directly assigned to the root element, allowing you to access and manipulate it as needed.
See core props for additional props that can be applied to this component.
proptyperequireddescriptiondefault
onRequestDismiss() => voidtrue
Callback function which will be invoked when the modal wants to be dismissed.
null
variant"standard" | "menu" | "panel-right" | "panel-left" | "panel-bottom" | "panel-top" | nullfalse
Specify Modal type
standard
isOpenboolean | nullfalse
Show/hide dialog
null
takeOverboolean | nullfalse
Decide whether to fill the full screen.
null
bodyWidth"standard" | "capped" | "grow" | nullfalse
Width of the dialog body. Some dialog variants allow the body width to be limited (capped)
null
onlyRenderWhenOpenboolean | nullfalse
only renders the contents of the dialog if the modal is open this is different than native dialog behavior, where the content is rendered but hidden
false
Was this page useful?

Thanks for your feedback!

Feel free to include your name if you wish to have a follow up conversation.


Published: Jan 7, 2022Last updated: Sep 12, 2024