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

Figma:

Responsive:

Adaptive:

Props

Component props
Name
Type
Default
accessibilityModalLabel
Required
string
-

String that clients such as VoiceOver will read to describe the modal. Always localize the label. See Accessibility section for more info.

onDismiss
Required
() => void
-

Callback fired when Modal is dismissed by clicking on the backdrop outside of the Modal (if closeOnOutsideClick is true).

align
"start" | "center"
"center"

Specify the alignment of heading & subHeading strings. See the Heading variant for more info.

children
React.Node
-

Supply the element(s) that will be used as Modal's main content. See the Best Practices for more info.

closeOnOutsideClick
boolean
true

Close the modal when you click outside of it. See the outside click variant for more info.

React.Node
-

Supply the element(s) that will be used as Modal's custom footer. See the Best Practices for more info.

heading
React.Node
-

The text used for Modal's heading. See the Heading variant for more info.

padding
"default" | "none"
"default"

The main Modal content has a "default" padding. For those cases where full bleed is needed, set padding to "none".

role
"alertdialog" | "dialog"
"dialog"

The underlying ARIA role for the Modal. See the Accessibility Role section for more info.

size
"sm" | "md" | "lg" | number
"sm"

Determines the width of the Modal. See the size variant for more info.

sm: 540px | md: 720px | lg: 900px

subHeading
string
-

Subtext for Modal, only renders with heading strings. See the sub-heading variant for more info.

Usage guidelines

When to use
  • 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.
When not to use
  • Any time a separate, designated URL is desired.
  • Requesting large forms of information. Consider OverlayPanel 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

Do

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.

Do

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

Do

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.

Don't

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

Don't

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.

Don't

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, OverlayPanel, 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 { Fragment, useState } from 'react';
import {
  Box,
  Button,
  CompositeZIndex,
  FixedZIndex,
  Flex,
  Heading,
  IconButton,
  Layer,
  Modal,
  RadioGroup,
} from 'gestalt';

export default function AccessibilityExample() {
  const [showComponent, setShowComponent] = useState(true);
  const [claim, setClaim] = useState('tag');

  const HEADER_ZINDEX = new FixedZIndex(10);
  const zIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Fragment>
      <Box padding={2}>
        <Button
          accessibilityLabel="Show Modal"
          color="red"
          onClick={() => setShowComponent(true)}
          size="lg"
          text="Show Modal"
        />
      </Box>
      {showComponent ? (
        <Layer zIndex={zIndex}>
          <Modal
            accessibilityModalLabel="Choose how to claim site"
            align="start"
            footer={
              <Flex gap={2} justifyContent="end">
                <Button color="gray" text="Cancel" />
                <Button color="red" text="Next" />
              </Flex>
            }
            heading={
              <Flex justifyContent="between">
                <Heading accessibilityLevel={1} size="500">
                  Pick claim option
                </Heading>
                <IconButton
                  accessibilityLabel="Dismiss modal"
                  bgColor="white"
                  icon="cancel"
                  iconColor="darkGray"
                  onClick={() => setShowComponent(false)}
                  size="sm"
                />
              </Flex>
            }
            onDismiss={() => {
              setShowComponent(false);
            }}
            size="sm"
          >
            <RadioGroup
              id="claim-option"
              legend="Claim options"
              legendDisplay="hidden"
            >
              <RadioGroup.RadioButton
                checked={claim === 'tag'}
                helperText="Paste this tag into the <head> section of your site's index.html file"
                id="claimTag"
                label="Add HTML tag"
                name="claim-type"
                onChange={() => setClaim('tag')}
                value="tag"
              />
              <RadioGroup.RadioButton
                checked={claim === 'file'}
                helperText="Download this file and upload it to your website's root directory"
                id="claimFile"
                label="Upload HTML file"
                name="claim-type"
                onChange={() => setClaim('file')}
                value="file"
              />
            </RadioGroup>
          </Modal>
        </Layer>
      ) : null}
    </Fragment>
  );
}

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.

role="alertdialog"
import { Fragment, useState } from 'react';
import {
  Box,
  Button,
  CompositeZIndex,
  FixedZIndex,
  Layer,
  ModalAlert,
  Text,
} from 'gestalt';

export default function AlertDialogAccessibilityExample() {
  const [showComponent, setShowComponent] = useState(true);

  const HEADER_ZINDEX = new FixedZIndex(10);
  const zIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Fragment>
      <Box padding={2}>
        <Button
          accessibilityLabel="Show Modal"
          color="red"
          onClick={() => setShowComponent(true)}
          size="lg"
          text="Show Modal"
        />
      </Box>
      {showComponent ? (
        <Layer zIndex={zIndex}>
          <ModalAlert
            accessibilityModalLabel="Delete 70s couch item"
            heading="Remove this item?"
            onDismiss={() => {
              setShowComponent(!showComponent);
            }}
            primaryAction={{
              accessibilityLabel: 'Remove item',
              label: 'Yes, remove',
              onClick: () => {},
              role: 'button',
            }}
            secondaryAction={{
              accessibilityLabel: 'Keep item',
              label: 'No, keep',
              onClick: () => {},
              role: 'button',
            }}
          >
            <Text>
              This item and all of its related metadata will be removed from
              your Catalogs permanently. This cannot be undone.
            </Text>
          </ModalAlert>
        </Layer>
      ) : null}
    </Fragment>
  );
}

role="dialog" (default)
import { Fragment, useState } from 'react';
import {
  Box,
  Button,
  Checkbox,
  CompositeZIndex,
  FixedZIndex,
  Flex,
  Layer,
  Modal,
  TextField,
} from 'gestalt';

export default function HeadingExample() {
  const [showComponent, setShowComponent] = useState(true);
  const HEADER_ZINDEX = new FixedZIndex(10);
  const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Box padding={8}>
      <Button onClick={() => setShowComponent(true)} text="View Modal" />
      {showComponent && (
        <Layer zIndex={modalZIndex}>
          <Modal
            accessibilityModalLabel="Create new board"
            align="start"
            footer={
              <Flex alignItems="center" justifyContent="end">
                <Button color="red" text="Create" />
              </Flex>
            }
            heading="Create board"
            onDismiss={() => setShowComponent(false)}
            size="sm"
          >
            <Fragment>
              <Box marginBottom={6}>
                <TextField
                  id="name"
                  label="Name"
                  onChange={() => {}}
                  placeholder='Like "Places to go" or "Recipes to Make"'
                  type="text"
                />
              </Box>
              <Checkbox
                checked={false}
                helperText="So only you and collaborators can see it."
                id="secret"
                label="Keep this board secret"
                name="languages"
                onChange={() => {}}
              />
            </Fragment>
          </Modal>
        </Layer>
      )}
    </Box>
  );
}

Localization

Be sure to localize all text strings. Note that localization can lengthen text by 20 to 30 percent.

Modal depends on DefaultLabelProvider for internal text strings. Localize the texts via DefaultLabelProvider. Learn more
import { Fragment, useState } from 'react';
import {
  Box,
  Button,
  Checkbox,
  CompositeZIndex,
  DefaultLabelProvider,
  FixedZIndex,
  Flex,
  Layer,
  Modal,
  TextField,
} from 'gestalt';

export default function Example() {
  const [showComponent, setShowComponent] = useState(true);
  const HEADER_ZINDEX = new FixedZIndex(10);
  const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <DefaultLabelProvider
      // $FlowExpectedError[incompatible-type] For demostration purposes
      labels={{
        Modal: {
          accessibilityDismissButtonLabel: 'Modal entlassen',
        },
      }}
    >
      <Box padding={8}>
        <Button onClick={() => setShowComponent(true)} text="Modal entlassen" />
        {showComponent && (
          <Layer zIndex={modalZIndex}>
            <Modal
              accessibilityModalLabel="Neue Tafel erstellen"
              align="start"
              footer={
                <Flex alignItems="center" justifyContent="end">
                  <Button color="red" text="Erstellen" />
                </Flex>
              }
              heading="Neue Tafel erstellen"
              onDismiss={() => setShowComponent(false)}
              size="sm"
            >
              <Fragment>
                <Box marginBottom={6}>
                  <TextField
                    id="name"
                    label="Name"
                    onChange={() => {}}
                    placeholder='Zum Beispiel "Ausflugsziele" oder "Rezepte"'
                    type="text"
                  />
                </Box>
                <Checkbox
                  checked={false}
                  helperText="Nur Sie und Ihre Mitarbeiter können es sehen."
                  id="secret"
                  label="Dieses Board geheim halten"
                  name="secret"
                  onChange={() => {}}
                />
              </Fragment>
            </Modal>
          </Layer>
        )}
      </Box>
    </DefaultLabelProvider>
  );
}

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 { Fragment, useState } from 'react';
import {
  Box,
  Button,
  Checkbox,
  CompositeZIndex,
  FixedZIndex,
  Flex,
  Layer,
  Modal,
  TextField,
} from 'gestalt';

export default function HeadingExample() {
  const [showComponent, setShowComponent] = useState(true);
  const HEADER_ZINDEX = new FixedZIndex(10);
  const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Box padding={8}>
      <Button onClick={() => setShowComponent(true)} text="View Modal" />
      {showComponent && (
        <Layer zIndex={modalZIndex}>
          <Modal
            accessibilityModalLabel="Create new board"
            align="start"
            footer={
              <Flex alignItems="center" justifyContent="end">
                <Button color="red" text="Create" />
              </Flex>
            }
            heading="Create board"
            onDismiss={() => setShowComponent(false)}
            size="sm"
          >
            <Fragment>
              <Box marginBottom={6}>
                <TextField
                  id="name"
                  label="Name"
                  onChange={() => {}}
                  placeholder='Like "Places to go" or "Recipes to Make"'
                  type="text"
                />
              </Box>
              <Checkbox
                checked={false}
                helperText="So only you and collaborators can see it."
                id="secret"
                label="Keep this board secret"
                name="languages"
                onChange={() => {}}
              />
            </Fragment>
          </Modal>
        </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 { useState } from 'react';
import {
  Box,
  Button,
  CompositeZIndex,
  FixedZIndex,
  Flex,
  Layer,
  Modal,
  Text,
} from 'gestalt';

export default function Example() {
  const [showComponent, setShowComponent] = useState(true);
  const HEADER_ZINDEX = new FixedZIndex(10);
  const modalZIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Box padding={8}>
      <Button onClick={() => setShowComponent(true)} text="View Modal" />
      {showComponent && (
        <Layer zIndex={modalZIndex}>
          <Modal
            accessibilityModalLabel="Resume account creation"
            align="start"
            footer={
              <Flex alignItems="center" gap={2} justifyContent="end">
                <Button onClick={() => setShowComponent(false)} text="Cancel" />
                <Button color="red" text="Resume" />
              </Flex>
            }
            heading="Resume your work?"
            onDismiss={() => setShowComponent(false)}
            size="sm"
            subHeading="Welcome back to the business account creation process!"
          >
            <Text>
              Want to continue where you left off? Click &quot;Resume&quot; to
              continue creating your account or &quot;Cancel&quot; to start
              over.
            </Text>
          </Modal>
        </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 { useReducer } from 'react';
import {
  Box,
  Button,
  CompositeZIndex,
  FixedZIndex,
  Heading,
  Layer,
  Modal,
} 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] = useReducer(reducer, initialState);
  const HEADER_ZINDEX = new FixedZIndex(10);
  const zIndex = new CompositeZIndex([HEADER_ZINDEX]);

  return (
    <Box marginEnd={-1} marginStart={-1} padding={8}>
      <Box padding={1}>
        <Button
          onClick={() => {
            dispatch({ type: 'small' });
          }}
          text="Small Modal"
        />
        {state.modal === 'small' && (
          <Layer zIndex={zIndex}>
            <Modal
              accessibilityModalLabel="View default padding and styling"
              footer={<Heading size="500">Footer</Heading>}
              heading="Small modal"
              onDismiss={() => {
                dispatch({ type: 'none' });
              }}
              size="sm"
            >
              <Heading size="500">Children</Heading>
            </Modal>
          </Layer>
        )}
      </Box>
      <Box padding={1}>
        <Button
          onClick={() => {
            dispatch({ type: 'medium' });
          }}
          text="Medium Modal"
        />
        {state.modal === 'medium' && (
          <Layer zIndex={zIndex}>
            <Modal
              accessibilityModalLabel="View default padding and styling"
              footer={<Heading size="500">Footer</Heading>}
              heading="Medium modal"
              onDismiss={() => {
                dispatch({ type: 'none' });
              }}
              size="md"
            >
              <Box padding={6}>
                <Heading size="500">Children</Heading>
              </Box>
            </Modal>
          </Layer>
        )}
      </Box>
      <Box padding={1}>
        <Button
          onClick={() => {
            dispatch({ type: 'large' });
          }}
          text="Large Modal"
        />
        {state.modal === 'large' && (
          <Layer zIndex={zIndex}>
            <Modal
              accessibilityModalLabel="View default padding and styling"
              footer={<Heading size="500">Footer</Heading>}
              heading="Large modal"
              onDismiss={() => {
                dispatch({ type: 'none' });
              }}
              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 { Fragment, useState } from 'react';
import { Box, Button, Flex, Layer, Modal, Text } from 'gestalt';

export default function PreventCloseExample() {
  const [showComponent, setShowComponent] = useState(true);
  return (
    <Fragment>
      <Box padding={8}>
        <Button
          onClick={() => {
            setShowComponent(!showComponent);
          }}
          text="Open Modal"
        />
      </Box>

      {showComponent && (
        <Layer>
          <Modal
            accessibilityModalLabel="Non closable modal"
            align="start"
            closeOnOutsideClick={false}
            footer={
              <Flex justifyContent="end">
                <Button
                  color="red"
                  onClick={() => {
                    setShowComponent(!showComponent);
                  }}
                  text="Close"
                />
              </Flex>
            }
            heading="Heading"
            onDismiss={() => {
              setShowComponent(!showComponent);
            }}
          >
            <Text align="start">Click on the button to close the modal</Text>
          </Modal>
        </Layer>
      )}
    </Fragment>
  );
}

Mobile

Modal requires DeviceTypeProvider to enable its mobile user interface. The example below shows the mobile platform UI and its implementation.

For mobile, all sizes are unified into a full mobile viewport Modal. Notice that subHeading gets moved from the header to the main content container.

import { useState } from 'react';
import {
  Box,
  Button,
  CompositeZIndex,
  DeviceTypeProvider,
  FixedZIndex,
  Flex,
  Layer,
  Modal,
  Text,
} from 'gestalt';

export default function Example() {
  const [showComponent, setShowComponent] = useState(true);
  const HEADER_ZINDEX = new FixedZIndex(10);

  return (
    <DeviceTypeProvider deviceType="mobile">
      {showComponent ? (
        <Layer zIndex={new CompositeZIndex([HEADER_ZINDEX])}>
          <Modal
            accessibilityModalLabel="Mobile Modal example"
            align="center"
            footer={
              <Flex gap={2} justifyContent="center">
                <Button color="gray" text="Secondary" />
                <Button color="red" text="Primary" />
              </Flex>
            }
            heading="Heading"
            onDismiss={() => setShowComponent(false)}
            size="lg"
            subHeading="SubHeading"
          >
            <Box>{Array(100).fill(<Text>Content</Text>)}</Box>
          </Modal>
        </Layer>
      ) : null}
      <Box padding={2}>
        <Button
          accessibilityLabel="Show Modal"
          color="red"
          onClick={() => setShowComponent(true)}
          size="lg"
          text="Show Modal"
        />
      </Box>
    </DeviceTypeProvider>
  );
}

Component quality checklist

Component quality checklist
Quality item
Status
Status description
Figma Library
Ready
Component is available in Figma for web and mobile web.
Responsive Web
Ready
Component responds to changing viewport sizes in web and mobile web.

Internal documentation

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.

OverlayPanel
To allow users to view optional information or complete sub-tasks in a workflow while keeping the context of the current page, use OverlayPanel.

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.