A Modal displays content that requires user interaction. Modals appear on a layer above the page and therefore block the content underneath, preventing users from interacting with anything else besides the Modal. Modal should be used to gather short bits of information from the user. For confirmation of an action or acknowledgment, use ModalAlert.
also known as Dialog, Prompt
Props
Usage guidelines
- Interrupting users to get confirmation on a user-triggered action.
- Requesting minimal amounts of information from a user (1-2 fields only).
- Capturing user's full attention for something important.
- Any time a separate, designated URL is desired.
- Requesting large forms of information. Consider a Sheet or new page instead.
- Any action that should not interrupt users from their current work stream.
- On top of another modal, since this can create usability issues and confusion.
Best practices
Use Modal when a response is required from the user. Clearly communicate what response is expected and make the action simple and straight forward, such as clicking a button to confirm. The most common responses will be related to confirming or canceling.
Limit the number of actions in a Modal. A primary and secondary action should be used for Modals. The rarely used tertiary actions are often destructive, such as “Delete”.
In the few cases where Modals are being used within the Pinner product, aim to prevent the content from needing to scroll at a reasonable screen size.
Use Modal for content that should have a dedicated surface, like login flows. Think about the core areas of your product that could appear in navigation. If a dedicated URL would be beneficial, use a full page instead. If the user interaction is an optional sub-task, consider using a Sheet.
Use Modal for long and complex tasks. Don’t keep the user in a Modal that takes multiple steps to exit. If multiple tasks are required, take the user to a separate page instead.
Add additional task-based Modals to the Pinner product. While these are currently used in some Pinner surfaces for editing, consider using a full page, Sheet, Flyout or inline editing for a better user experience.
Accessibility
Labels
We want to make sure Modals have a clear purpose when being read by a screen reader. accessibilityModalLabel
allows us to update the spoken text for the heading prop and give it more context.
import React from 'react'; import { Box, Button, CompositeZIndex, FixedZIndex, Flex, IconButton, Heading, Layer, Modal, RadioGroup, } from 'gestalt'; export default function AccessibilityExample() { const [showModal, setShowModal] = React.useState(false); const [claim, setClaim] = React.useState('tag'); const HEADER_ZINDEX = new FixedZIndex(10); const zIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Layer zIndex={zIndex}> <Modal accessibilityModalLabel="Choose how to claim site" align="start" heading={ <Box padding={6}> <Flex justifyContent="between"> <Heading size="500" accessibilityLevel={1}> Pick claim option </Heading> <IconButton accessibilityLabel="Dismiss modal" bgColor="white" icon="cancel" iconColor="darkGray" onClick={() => {}} size="sm" /> </Flex> </Box> } onDismiss={() => { setShowModal(!showModal); }} footer={ <Flex justifyContent="end" gap={2}> <Button color="gray" text="Cancel" /> <Button color="red" text="Next" /> </Flex> } size="sm" > <Box padding={6}> <RadioGroup id="claim-option" legend="Claim options" legendDisplay="hidden" > <RadioGroup.RadioButton checked={claim === 'tag'} id="claimTag" label="Add HTML tag" helperText="Paste this tag into the <head> section of your site's index.html file" name="claim-type" onChange={() => setClaim('tag')} value="tag" /> <RadioGroup.RadioButton checked={claim === 'file'} id="claimFile" label="Upload HTML file" helperText="Download this file and upload it to your website's root directory" name="claim-type" onChange={() => setClaim('file')} value="file" /> </RadioGroup> </Box> </Modal> </Layer> ); }
Role
If the Modal requires the user’s immediate attention, such as an error or warning, use the ModalAlert component instead, which automatically applies role="alertdialog"
to the modal. For instance, navigating away from a page with active edits may trigger an alertdialog ModalAlert that asks the user to confirm if they want to lose their changes. Learn more about the alertdialog role.
import React from 'react'; import { CompositeZIndex, FixedZIndex, Layer, ModalAlert, Text } from 'gestalt'; export default function AlertDialogAccessibilityExample() { const [showModal, setShowModal] = React.useState(false); const HEADER_ZINDEX = new FixedZIndex(10); const zIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Layer zIndex={zIndex}> <ModalAlert accessibilityModalLabel="Delete 70s couch item" heading="Remove this item?" primaryAction={{ accessibilityLabel: 'Remove item', label: 'Yes, remove', onClick: () => {}, }} secondaryAction={{ accessibilityLabel: 'Keep item', label: 'No, keep', onClick: () => {}, }} onDismiss={() => { setShowModal(!showModal); }} > <Text> This item and all of its related metadata will be removed from your Catalogs permanently. This cannot be undone. </Text> </ModalAlert> </Layer> ); }
import React from 'react'; import { Box, Button, Checkbox, CompositeZIndex, FixedZIndex, Flex, Layer, Modal, TextField, } from 'gestalt'; function ModalWithHeading({ onDismiss }) { return ( <Modal accessibilityModalLabel="Create new board" align="start" heading="Create board" onDismiss={onDismiss} footer={ <Flex alignItems="center" justifyContent="end"> <Button color="red" text="Create" /> </Flex> } size="sm" > <Box paddingX={6}> <Box marginBottom={6}> <TextField id="name" onChange={() => {}} placeholder='Like "Places to go" or "Recipes to Make"' label="Name" type="text" /> </Box> <Checkbox checked={false} id="secret" label="Keep this board secret" helperText="So only you and collaborators can see it." name="languages" onChange={() => {}} /> </Box> </Modal> ); } export default function HeadingExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Box padding={8}> <Button text="View Modal" onClick={() => setShouldShow(true)} /> {shouldShow && ( <Layer zIndex={modalZIndex}> <ModalWithHeading onDismiss={() => setShouldShow(false)} /> </Layer> )} </Box> ); }
Localization
Be sure to localize the heading
, subheading
and accessibilityModalLabel
props, as well as any other text elements within Modal. Note that localization can lengthen text by 20 to 30 percent.
Variants
Heading
The heading
will render an H1 when a string is passed in and supports multiple alignment options with the align
prop.
Start
start
aligned text is the primary alignment for our Business products. It will be left-aligned in left-to-right languages and right-aligned in right-to-left languages.Center
center
aligned text is the primary alignment for our Pinner products.Custom
If you need more control over the Modal heading, you can pass a custom React node as the heading prop and the Modal will render that instead. This feature should be used sparingly as most customization should be added to the content area. Please contact the Gestalt team if this is needed for your product.
import React from 'react'; import { Box, Button, Checkbox, CompositeZIndex, FixedZIndex, Flex, Layer, Modal, TextField, } from 'gestalt'; function ModalWithHeading({ onDismiss }) { return ( <Modal accessibilityModalLabel="Create new board" align="start" heading="Create board" onDismiss={onDismiss} footer={ <Flex alignItems="center" justifyContent="end"> <Button color="red" text="Create" /> </Flex> } size="sm" > <Box paddingX={6}> <Box marginBottom={6}> <TextField id="name" onChange={() => {}} placeholder='Like "Places to go" or "Recipes to Make"' label="Name" type="text" /> </Box> <Checkbox checked={false} id="secret" label="Keep this board secret" helperText="So only you and collaborators can see it." name="languages" onChange={() => {}} /> </Box> </Modal> ); } export default function HeadingExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Box padding={8}> <Button text="View Modal" onClick={() => setShouldShow(true)} /> {shouldShow && ( <Layer zIndex={modalZIndex}> <ModalWithHeading onDismiss={() => setShouldShow(false)} /> </Layer> )} </Box> ); }
Sub-heading
The subHeading
is a container that can be used for subtext that provides additional context for the Modal. The sub-heading locks to the top under the heading.
import React from 'react'; import { Box, Button, CompositeZIndex, FixedZIndex, Flex, Layer, Modal, Text, } from 'gestalt'; function ModalWithSubHeading({ onDismiss }) { return ( <Modal accessibilityModalLabel="Resume account creation" align="start" heading="Resume your work?" subHeading="Welcome back to the business account creation process!" onDismiss={onDismiss} footer={ <Flex alignItems="center" justifyContent="end" gap={2}> <Button text="Cancel" onClick={onDismiss} /> <Button color="red" text="Resume" /> </Flex> } size="sm" > <Box paddingX={6}> <Text> Want to continue where you left off? Click "Resume" to continue creating your account or "Cancel" to start over. </Text> </Box> </Modal> ); } export default function SubHeadingExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Box padding={8}> <Button text="View Modal" onClick={() => setShouldShow(true)} /> {shouldShow && ( <Layer zIndex={modalZIndex}> <ModalWithSubHeading onDismiss={() => setShouldShow(false)} /> </Layer> )} </Box> ); }
Sizes
Modal has 3 size options: small (sm
- 540px), medium (md
- 720px) and large (lg
- 900px). If absolutely necessary, a number representing a custom width can be provided instead, but we recommend using one of the standard sizes.
All Modals have a max-width of 100%.
import React from 'react'; import { Box, Button, CompositeZIndex, FixedZIndex, Layer, Modal, Heading, } from 'gestalt'; export default function SizesExample() { function reducer(state, action) { switch (action.type) { case 'small': return { modal: 'small' }; case 'medium': return { modal: 'medium' }; case 'large': return { modal: 'large' }; case 'none': return { modal: 'none' }; default: throw new Error(); } } const initialState = { modal: 'none' }; const [state, dispatch] = React.useReducer(reducer, initialState); const HEADER_ZINDEX = new FixedZIndex(10); const zIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <Box marginStart={-1} marginEnd={-1} padding={8}> <Box padding={1}> <Button text="Small Modal" onClick={() => { dispatch({ type: 'small' }); }} /> {state.modal === 'small' && ( <Layer zIndex={zIndex}> <Modal accessibilityModalLabel="View default padding and styling" heading="Small modal" onDismiss={() => { dispatch({ type: 'none' }); }} footer={<Heading size="500">Footer</Heading>} size="sm" > <Box padding={6}> <Heading size="500">Children</Heading> </Box> </Modal> </Layer> )} </Box> <Box padding={1}> <Button text="Medium Modal" onClick={() => { dispatch({ type: 'medium' }); }} /> {state.modal === 'medium' && ( <Layer zIndex={zIndex}> <Modal accessibilityModalLabel="View default padding and styling" heading="Medium modal" onDismiss={() => { dispatch({ type: 'none' }); }} footer={<Heading size="500">Footer</Heading>} size="md" > <Box padding={6}> <Heading size="500">Children</Heading> </Box> </Modal> </Layer> )} </Box> <Box padding={1}> <Button text="Large Modal" onClick={() => { dispatch({ type: 'large' }); }} /> {state.modal === 'large' && ( <Layer zIndex={zIndex}> <Modal accessibilityModalLabel="View default padding and styling" heading="Large modal" onDismiss={() => { dispatch({ type: 'none' }); }} footer={<Heading size="500">Footer</Heading>} size="lg" > <Box padding={6}> <Heading size="500">Children</Heading> </Box> </Modal> </Layer> )} </Box> </Box> ); }
Preventing close on outside click
By default, users can click outside the Modal (on the overlay) to close it. This can be disabled by setting closeOnOutsideClick
to false. In most cases, the user should be prevented from closing the Modal if the action is required.
import React from 'react'; import { Box, Button, Flex, Layer, Modal, Text } from 'gestalt'; export default function PreventCloseExample() { const [showModal, setShowModal] = React.useState(false); return ( <React.Fragment> <Box padding={8}> <Button text="Open Modal" onClick={() => { setShowModal(!showModal); }} /> </Box> {showModal && ( <Layer> <Modal accessibilityModalLabel="Non closable modal" align="start" closeOnOutsideClick={false} heading="Heading" onDismiss={() => { setShowModal(!showModal); }} footer={ <Flex justifyContent="end"> <Button color="red" text="Close" onClick={() => { setShowModal(!showModal); }} /> </Flex> } > <Box padding={6}> <Text align="start">Click on the button to close the modal</Text> </Box> </Modal> </Layer> )} </React.Fragment> ); }
Component quality checklist
Quality item | Status | Status description |
---|---|---|
Figma Library | Partially ready | Component is live in Figma, however may not be available for all platforms. |
Responsive Web | Ready | Component is available in code for web and mobile web. |
iOS | Component is not currently available in code for iOS. | |
Android | Component is not currently available in code for Android. |
Related
ModalAlert
Used to alert a user of an issue, or to request confirmation after a user-triggered action. Should be used instead of Modal for simple acknowledgments and confirmations.
Sheet
To allow users to view optional information or complete sub-tasks in a workflow while keeping the context of the current page, use Sheet.
Toast
Toast provides temporary feedback on an interaction. Toasts appear at the bottom of a desktop screen or top of a mobile screen, instead of blocking the entire page.