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 theModalDialogCloseButton
(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 theModalDialogFooter
and not theModalDialogBody.
- 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 inswan-modal-dialog-body
, and your buttons should be wrapped inswan-modal-dialog-footer
.
React
The React ModalDialog
requires 2 props:
isOpen
- whether or not theModalDialog
should be open.onRequestDismiss
- a callback function which will be invoked when theModalDialog
wants to be dismissed. This can happen in a number of different ways e.g. user clicked on theModalDialogCloseButton
, 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 classswan-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.
- a wrapper
- 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.
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 aModalDialogFooter
component, to ensure consistent spacing.
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
andmenu
dialogs should use thepinned
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 isbodyWidth="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.
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.
Menu
- The
menu
option is for left-hand flyout menus, such as the site navigation on mobile devices. - All
panel
andmenu
dialogs should use thepinned
option that pins their footers to the bottom of the panel. In React, this is<ModalDialogFooter pinned>
.
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.
Pinned footer
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.
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.
Grow
The grow
option will make the modal dialog grow as wide as the content requires, up to almost the full browser width.
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.
Takeover
Setting the takeOver
option will cause the dialog to fill the full screen on all screen sizes. (Any dialog, even those without this option, will cover the full screen on Extra-Small displays.)
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.
Background color
If you want to put a background color on a dialog, put it into the ModalDialog
element.
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.
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.
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 nomodal-dialog-open
state associated with the now-current history entry, soisOpen
is set tofalse
(the default value).{replace:true}
be used as a third parameter in the functionuseBrowserHistoryState()
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`:
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.