Vistaprint
scriptKeysmodalDialogstyleKeysmodalDialog

Modal Dialog

A Modal Dialog is a secondary window that appears in a layer above the application, to display content that demands user action.

Playground

API Update for visage-core 4.16 and react-visage 4.25:

Modal Dialog has a new, updated API to support our new modal dialog designs. This change is backwards-compatible, and the older API still works -- but it is now deprecated, so please update your Modal Dialogs:

  • If you are using the React API, there is a new 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).

  • 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 a new 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 JS API, you now need an empty stylized-dialog-nav at the top of the dialog, your dialog's main content should be wrapped in stylized-dialog-body, and your buttons should be wrapped in stylized-dialog-footer.

Usage

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"

Share
Share
jsx

vanilla

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 dialog's code will move it to be a direct child of the <body> tag when it opens.

The element that launches the dialog must have a data-dialog-show attribute whose value is the id of the dialog.

markup
<dialog
class="stylized-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="myExampleTitle"
id="myExampleStylizedDialog"
>
<div class="stylized-dialog-nav"></div>
<div class="stylized-dialog-header">
<!-- header is an optional element -->
<h2 class="stylized-dialog-title" id="myExampleTitle">Dialog Title</h2>
</div>
<div class="stylized-dialog-body">Dialog body goes here</div>
<div class="stylized-dialog-footer">
<!-- footer is an optional element -->
Footer is an optional element (usually a "buttons" element with Textbuttons
in it)
</div>
</dialog>
<!-- the element that launches the dialog can be any tag, not just a <button> tag -->
<button data-dialog-show="myExampleStylizedDialog">Open the dialog</button>

Dialog Buttons

Many Modal Dialogs will have a button or two at the bottom, e.g. "Cancel"/"Confirm". You should render these buttons inside of a Modal Dialog Buttons component, itself inside a Modal Dialog Footer component. This assures that the buttons have the proper amount of space around them.

Most dialogs should use Textbuttons with the "mini" height. Only dialogs that sit at the left or right sides of the window ("panel" or "menu" dialogs) should use standard-height buttons.

Buttons can use whatever width is appropriate for your design; they can use the "full-width" option that makes Textbuttons fill their available width.

Share
Share
jsx
<dialog
class="stylized-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="exampleStylizedDialog1Title"
>
<div class="stylized-dialog-header">
<h2 class="stylized-dialog-title" id="exampleStylizedDialog1Title">
My Modal Dialog
</h2>
</div>
<div class="stylized-dialog-body">
<p>Some content inside of the Modal Dialog</p>
</div>
<div class="stylized-dialog-footer">
<div class="stylized-dialog-buttons">
<button
class="textbutton textbutton-skin-secondary stylized-dialog-close"
>
Cancel
</button>
<button class="textbutton textbutton-skin-primary stylized-dialog-close">
OK
</button>
</div>
</div>
</dialog>

You can also place labeling text to the left of a textbutton using the "Modal Dialog Buttons Text" sub-component. This sub-component can contain any typography you want, and even a link:

Share
Share
jsx

Panel

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

  • "panel-right" dialogs slide out from the right side, and will be as tall as the screen. They are used to provide additional detail and/or choices.

  • "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.

  • "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.

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

Share
Share
jsx
jsx
<ModalDialog variant="panel-right">...</ModalDialog>
<dialog
class="stylized-dialog stylized-dialog-panel"
role="dialog"
aria-modal="true"
>
...
</dialog>

The "menu" option is for left-hand flyout menus, such as the site navigation on mobile devices.

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.

Share
Share
jsx
jsx
<ModalDialog variant="menu">...</ModalDialog>
<dialog
class="
stylized-dialog
stylized-dialog-panel
stylized-dialog-panel-left
stylized-dialog-panel-menu
"
role="dialog"
aria-modal="true"
>
...
</dialog>

"Panel" and "Menu" dialogs allow a Modal Dialog Footer to be "pinned" to the bottom of the dialog, always staying visible as the user scrolls through the dialog content. While technically optional, this is now the recommended layout for any panel or menu dialog with a footer.

(Note: some browsers such as Firefox can't support this feature. They will show the footer right below the dialog body, as normal.)

Share
Share
jsx
jsx
<ModalDialogFooter pinned></ModalDialogFooter>
<dialog
class="stylized-dialog stylized-dialog-panel stylized-dialog-panel-pinned"
role="dialog"
aria-modal="true"
>
<div class="stylized-dialog-footer">...</div>
</dialog>

"Capped" panels

"Panel" dialogs can set their body width to be capped, limiting how wide they can get. 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.

Share
Share
jsx
<dialog
class="
stylized-dialog
stylized-dialog-panel
stylized-dialog-panel-capped
stylized-dialog-panel-pinned
"
role="dialog"
aria-modal="true"
aria-labelledby="myExampleHeaderPanelTitle"
></dialog>

Panel with a custom nav

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

Share
Share
jsx

Full Bleed

The "full bleed" option makes the dialog's content touch the dialog's edges by removing the padding around the dialog content.

Share
Share
jsx
jsx
<ModalDialogContent fullBleed>...</ModalDialogContent>
<dialog
class="stylized-dialog stylized-dialog-skin-full-bleed"
role="dialog"
aria-modal="true"
>
...
</dialog>

Takeover

Setting the "takeover" option will cause the dialog to fill the full screen.

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.

Share
Share
jsx
jsx
<ModalDialog takeOver>...</ModalDialog>
<dialog
class="stylized-dialog stylized-dialog-takeover"
role="dialog"
aria-modal="true"
>
...
</dialog>

Takeover dialogs can also use the translucent option, which changes the background color of the dialog to a semi-transparent blue color.

Share
Share
jsx
jsx
<ModalDialog takeOver translucent>
...
</ModalDialog>
<dialog
class="
stylized-dialog
stylized-dialog-takeover
stylized-dialog-takeover-translucent
"
role="dialog"
aria-modal="true"
>
...
</dialog>

No close button

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

jsx
<>{/*{just leave out the <ModalDialogCloseButton /> element}*/}</>
<!-- the vanilla API adds the close button automatically, so you need to explicitly suppress it -->
<dialog
class="stylized-dialog stylized-dialog-no-close-button"
role="dialog"
aria-modal="true"
></dialog>

Browser History

Sometimes, we want our Modal Dialog 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.

typescript
function useBrowserHistoryState<T>(
key: string,
defaultValue: T,
): [T, React.Dispatch<React.SetStateAction<T>>]

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.

Share
Share
jsx

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).

jsx
() => {
const [isOpen, setIsOpen] = useBrowserHistoryState('modal-dialog-open', false)
return (
<>
<TextButton onClick={() => setIsOpen(true)}>Open</TextButton>
<ModalDialog isOpen={isOpen} onRequestDismiss={() => setIsOpen(false)}>
<ModalDialogContent>
<ModalDialogNav>
<ModalDialogCloseButton visuallyHiddenLabel="Close" />
{/* Close Button label text must be localized! */}
</ModalDialogNav>
<ModalDialogHeader>
<ModalDialogTitle>My Modal Dialog</ModalDialogTitle>
</ModalDialogHeader>
<ModalDialogBody>
Some content inside of the ModalDialog
</ModalDialogBody>
</ModalDialogContent>
</ModalDialog>
</>
)
}

vanilla JS

The "data-dialog-enable-browser-history" attribute allows modal dialogs to be closed with the browser's back button:

markup
<dialog
class="stylized-dialog"
role="dialog"
aria-modal="true"
data-dialog-enable-browser-history="all"
></dialog>

Components

ModalDialog

PropTypeDefaultDescription
isOpenbooleanrequired

Whether or not the ModalDialog is open

onRequestDismiss() => voidrequired

Callback invoked when the ModalDialog wants to be dismissed

Could happen via Esc key, clicking on the backdrop, clicking on close button, ...

variant"menu" | "standard" | "panel-right" | "panel-left" | "panel-bottom" | "panel-top""standard"

The visual style of the ModalDialog

takeOverbooleanfalse

Optionally allow the dialog to fill the full screen

translucentbooleanfalse

Makes the background translucent. Should only be used with takeOver={true}

bodyWidth"standard" | "capped""standard"

Width of the dialog body. Some dialog variants allow the body width to be limited (capped)

allowPinchZoombooleantrue

Handle zoom/pinch gestures on devices when scroll locking is enabled.

All other props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogContent

PropTypeDefaultDescription
fullBleedbooleanfalse

Whether or not the content of the ModalDialog should fill all available space

All other props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogCloseButton

PropTypeDefaultDescription
visuallyHiddenLabelReactNoderequired

A localized label which describes the close button to screen-reader users

Typically it is a string like "Close"

All other props are forwarded to the element specified in thecomponentprop(default: <button/>)

ModalDialogHeader

ModalDialogHeader has no props of its own

All props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogTitle

ModalDialogTitle has no props of its own

All props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogBody

ModalDialogBody has no props of its own

All props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogButtons

ModalDialogButtons has no props of its own

All props are forwarded to the element specified in thecomponentprop(default: <div/>)

ModalDialogFooter

PropTypeDefaultDescription
pinnedbooleanfalse

Whether or not the footer should be pinned to the bottom of the ModalDialog

(Only compatible with variant="menu" ModalDialogs)

All other props are forwarded to the element specified in thecomponentprop(default: <div/>)

Guidelines

Designer Guidelines

  • Avoid dialogs that are so tall that they require scrolling. They make for a poor user experience on small screens such as phones. If the content of the dialog is tall, consider a form of progressive disclosure, such as tabs, or an accordion, or a nested scrollable container. Also consider re-formulating the content so it doesn't need to be so large, or that it can be split across a wizard flow.

  • Avoid any UX that involves having two modal dialogs open at once, such as a modal dialog that opens another modal dialog. This is a UX anti-pattern, and our modal dialog component was not designed to support it.

  • Because a modal dialog disables the application beneath it and may interrupt the user's workflow, use it for very important information -- information that when provided could lessen the user's effort, or if it maintains the context of a task, such as editing.

  • Do not place any SEO-relevant information in a modal dialog, since search engines can't see any information that requires user interaction to view. (Please consult the Organic Search team with any questions.)

  • Place the primary action button to the right of secondary actions, using primary and secondary button styling (see textbutton guidelines).

  • Use "mini" buttons on most dialogs. Only use standard-size buttons on "panel" or "menu" dialogs that slide out from the sides.

  • When adding more than one action button, follow this order: destructive (delete, sign out), dismissive (cancel), affirmative (confirm, sign in).

  • Best practices state that you should not use a modal dialog in the checkout flow.

  • Modal dialog content should be focused, and should not involve complex decision-making effort.

Developer Guidelines

  • 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.

  • The dialog's JavaScript code 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 specific to the 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 dialogs must have an id attribute on them.

  • You can specify a different max-width by putting a data-dialog-max-width attribute on the dialog, e.g. data-dialog-max-width="1000"

  • The dialog will be hidden by default. It will be shown when there is a click on an element with a data-dialog-show attribute whose value is the id of the dialog. You can also show it by calling the method showStylizedDialog() with the dialog's DOM node as the argument. Once the dialog has been shown, it will trigger a showStylizedDialog event. Do not use the native showModal() or show() methods, as these will cause display and functionality errors.

  • The dialog will close when there is a click on any internal element that has class stylized-dialog-close on it. You can also close it by calling the method closeStylizedDialog() with the dialog's DOM node as the argument. Do not use the native close() method. 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.

  • To have the dialog create an iframe inside it, place three attributes on the dialog: data-dialog-iframe-url (the URL of the iframe), data-dialog-iframe-id (the id of the iframe), and data-dialog-iframe-title (the title of the iframe).

    • The iframe will be appended to the end of the dialog's content, so you will likely want to have any of the dialog's buttons be inside the iframe.

    • While the iframe is loading, the user will see a preloader graphic, replaced by the full dialog once the iframe is ready.

Accessibility Guidelines

  • 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.

  • If you are using the React API, 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.

SEO Considerations

  • Panel dialogs must be rendered on the server, and part of the page's source HTML, to be readable by Googlebot.

  • Panel dialogs should contain less important content. Since they require an additional click to be seen by users and bots, the content appearing inside them may have less importance for ranking (especially on desktop).