Skip to contentSkip to navigationSkip to topbar
Paste assistant Assistant
Figma
Star

Modal Dialog Primitive

Version 2.0.1GithubStorybook

An unstyled and accessible basis upon which to build Modal Dialogs.


Guidelines

Guidelines page anchor

About the Modal Dialog Primitive

About the Modal Dialog Primitive page anchor

The modal dialog primitive is an unstyled and barebones version of a Modal dialog(link takes you to an external page). It handles the implementation details around accessibility and provides a robust API to build upon. For example, our Modal component is built on top of this primitive. If you find that our designed Modal component can’t work for your particular use case, we suggest falling back to this component to roll your own solution. We encourage using this code as a basis for all modal dialogs in order to avoid code fragmentation and accessibility issues in our products.

(information)

Looking for Paste's styled Modal?

Only use this primitive if you have a bespoke modal design. For most Twilio interfaces, we recommend using the Paste Modal component.

This package is a wrapper around @reach/dialog(link takes you to an external page). If you’re wondering why we wrapped that package into our own, we reasoned that it would be best for our consumers’ developer experience. With reasons such as:

  • We can control which APIs we expose and how to expose them. For example, in this package we rename and export only some of the source package's exports.
  • If we want to migrate the underlying nuts and bolts in the future, Twilio products that depend on this primitive would need to replace all occurrences of import … from ‘@reach/dialog’ to import … from ‘@some-new/package’. By wrapping it in @twilio-paste/modal-dialog-primitive, this refactor can be avoided. The only change would be a version bump in the package.json file.
  • We can more strictly enforce semver and backwards compatibility than some of our dependencies.
  • We can control when to provide an update and which versions we allow, to help reduce potential bugs our consumers may face.

Installation

Installation page anchor

This package is available individually or as part of @twilio-paste/core.

yarn add @twilio-paste/modal-dialog-primitive - or - yarn add @twilio-paste/core
import * as React from 'react';
import {styled} from '@twilio-paste/styling-library';
import {Text} from '@twilio-paste/text';
import {Button} from '@twilio-paste/button';
import {ModalDialogPrimitiveOverlay, ModalDialogPrimitiveContent} from '@twilio-paste/modal-dialog-primitive';

const StyledModalDialogOverlay = styled(ModalDialogPrimitiveOverlay)({
  position: 'fixed',
  top: 0,
  right: 0,
  bottom: 0,
  left: 0,
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center',
  background: 'rgba(0, 0, 0, 0.7)',
});

const StyledModalDialogContent = styled(ModalDialogPrimitiveContent)({
  width: '100%',
  maxWidth: '560px',
  maxHeight: 'calc(100% - 60px)',
  background: '#f4f5f6',
  borderRadius: '5px',
  padding: '20px',
});

interface BasicModalDialogProps {
  isOpen: boolean;
  handleClose: () => void;
}

const BasicModalDialog: React.FC<BasicModalDialogProps> = ({isOpen, handleClose}) => {
  const inputRef = React.useRef();

  return (
    <StyledModalDialogOverlay
      isOpen={isOpen}
      onDismiss={handleClose}
      allowPinchZoom={true}
      initialFocusRef={inputRef}
    >
      <StyledModalDialogContent>
        <input type="text" value="first" />
        <br />
        <input ref={inputRef} type="text" value="second (initial focused)" />
        <Text as="p" color="colorText">
          Roll your own dialog!
        </Text>
      </StyledModalDialogContent>
    </StyledModalDialogOverlay>
  );
};

export const ModalActivator: React.FC = () => {
  const [isOpen, setIsOpen] = React.useState(false);
  const handleOpen = (): void => setIsOpen(true);
  const handleClose = (): void => setIsOpen(false);

  return (
    <div>
      <Button variant="primary" onClick={handleOpen}>
        Open Sample Modal
      </Button>
      <BasicModalDialog isOpen={isOpen} handleClose={handleClose} />
    </div>
  );
};
(information)

Much of the following is copied directly from reach-ui's docs. Because we may update at a different cadence, we're duplicating the docs here to prevent inconsistent behaviors.

ModalDialogPrimitiveOverlay Props

ModalDialogPrimitiveOverlay Props page anchor

All the regular HTML attributes (role, aria-*, type, and so on) including the following custom props:

PropTypeDefault
isOpenboolfalse
allowPinchZoom?boolfalse
onDismiss?(event) => voidnoop
initialFocusRef?refnull
childrennodenull
isOpen prop

Controls whether the dialog is open or not.

<ModalDialogPrimitiveOverlay isOpen={true}>
  <p>I will be open</p>
</ModalDialogPrimitiveOverlay>

<ModalDialogPrimitiveOverlay isOpen={false}>
  <p>I will be closed</p>
</ModalDialogPrimitiveOverlay>
allowPinchZoom prop

Controls whether the dialog should allow zoom/pinch gestures on iOS devices.

onDismiss prop

This function is called whenever the user hits "Escape" or clicks outside the dialog. It's important to close the dialog when onDismiss is fired, as seen in all the demos on this page.

The only time you shouldn't close the dialog onDismiss is when the dialog requires a choice and none of them are "cancel". For example, perhaps two records need to be merged and the user needs to pick the surviving record. Neither choice is less destructive than the other, in these cases you may want to alert the user they need to a make a choice onDismiss instead of closing the dialog.

function Example(props) {
  const [showDialog, setShowDialog] = React.useState(false);
  const open = () => setShowDialog(true);
  const close = () => setShowDialog(false);

  return (
    <div>
      <button onClick={open}>Show Dialog</button>
      <ModalDialogPrimitiveOverlay isOpen={showDialog} onDismiss={close}>
        <ModalDialogPrimitiveContent>
          <Text as="p">
            It is your job to close this with state when the user clicks outside
            or presses escape.
          </Text>
          <button onClick={close}>Okay</button>
        </ModalDialogPrimitiveContent>
      </ModalDialogPrimitiveOverlay>
    </div>
  );
}
function Example(props) {
  const [showDialog, setShowDialog] = React.useState(false);
  const [showWarning, setShowWarning] = React.useState(false);
  const open = () => {
    setShowDialog(true);
    setShowWarning(false);
  };
  const close = () => setShowDialog(false);
  const dismiss = () => setShowWarning(true);

  return (
    <div>
      <button onClick={open}>Show Dialog</button>
      <ModalDialogPrimitiveOverlay isOpen={showDialog} onDismiss={close}>
        <ModalDialogPrimitiveContent>
          {showWarning && (
            <p style={{ color: "red" }}>You must make a choice, sorry :(</p>
          )}
          <p>Which router should survive the merge?</p>
          <button onClick={close}>React Router</button>{" "}
          <button onClick={close}>@reach/router</button>
        </ModalDialogPrimitiveContent>
      </ModalDialogPrimitiveOverlay>
    </div>
  );
}
initialFocusRef prop

By default the first focusable element will receive focus when the dialog opens but you can provide a ref to focus instead.

function Example(props) {
  const [showDialog, setShowDialog] = React.useState(false);
  const buttonRef = React.useRef();
  const open = () => setShowDialog(true);
  const close = () => setShowDialog(false);

  return (
    <div>
      <button onClick={open}>Show Dialog</button>
      {showDialog && (
        <ModalDialogPrimitiveOverlay initialFocusRef={buttonRef} onDismiss={close}>
          <ModalDialogPrimitiveContent>
            <p>Pass the button ref to DialogOverlay and the button.</p>
            <button onClick={close}>Not me</button>
            <button
              ref={buttonRef}
              onClick={close}
            >
              Got me!
            </button>
          </ModalDialogPrimitiveContent>
        </ModalDialogPrimitiveOverlay>
      )}
    </div>
  );
}

ModalDialogPrimitiveContent Props

ModalDialogPrimitiveContent Props page anchor

All the regular HTML attributes (role, aria-*, type, and so on) including the following custom props:

PropTypeDefault
childrennodenull