Modal

Import
import { Modal } from "reshaped";
import type { ModalProps } from "reshaped";
Related components
Storybook

Modal is a controlled component, meaning it has an active property and multiple handlers to change this property's value. When the Modal is active, it prevents scrolling of the entire page while allowing scrolling for content inside the overlay. As a controlled component, you need to pass an onClose handler to update its visibility when users click outside the Modal or press Esc. Using it with the useToggle hook can reduce the amount of boilerplate needed to handle its state.

It's safe to keep Modal in your render all the time. The modal is rendered in the DOM only when it is active. Conditionally rendering the Modal will prevent its animation from working.

function Example() {
  const { active, activate, deactivate } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open modal</Button>
      <Modal active={active} onClose={deactivate}>
        Modal content
      </Modal>
    </>
  );
}

Besides the default center position, you can use Modal with the bottom, start, end and full-screen positions. This will change its animation to slide in and out from the respective side.

function ExamplePosition() {
  const { activate, deactivate, active } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open bottom sheet</Button>
      <Modal active={active} onClose={deactivate} position="bottom">
        Bottom sheet content
      </Modal>
    </>
  );
}

Modal comes with a default size that can be customized using the size property. You can pass any px or percent value as a string. For the bottom position, the size will change its height instead of its width.

function ExampleSize() {
  const { activate, deactivate, active } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open modal</Button>
      <Modal size="200px" active={active} onClose={deactivate}>
        Small modal content
      </Modal>
    </>
  );
}

Modal comes with a default padding that can be customized using a unit multiplier value. For example, you can set the padding to x2 with padding={2} or remove it altogether by setting the padding property to 0.

function ExampleWithoutPadding() {
  const { active, activate, deactivate } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open modal</Button>
      <Modal active={active} onClose={deactivate} padding={2}>
        Modal content
      </Modal>
    </>
  );
}

Modal supports Modal.Title and Modal.Subtitle compound components, which handle the aria attributes and provide default text styles. You can use them with the Dismissible utility to implement more complex modal layouts.

function ExampleWithDismissible() {
  const { active, activate, deactivate } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open modal</Button>
      <Modal active={active} onClose={deactivate}>
        <View gap={3}>
          <Dismissible onClose={deactivate} closeAriaLabel="Close modal">
            <Modal.Title>Modal title</Modal.Title>
            <Modal.Subtitle>Modal subtitle</Modal.Subtitle>
          </Dismissible>

          <View backgroundColor="neutral-faded" height={10} />
        </View>
      </Modal>
    </>
  );
}

When the Modal becomes active, user focus is automatically trapped and moved to the first element in the Modal content area. This is the expected behavior in most cases. However, you can also move the focus to any other element inside the Modal using refs.

function ExampleWithCustomFocus() {
  const { active, activate, deactivate } = useToggle(false);
  const inputRef = React.useRef(null);

  const handleOpen = () => inputRef.current.focus();

  return (
    <>
      <Button onClick={activate}>Open modal</Button>
      <Modal active={active} onClose={deactivate} onOpen={handleOpen}>
        <View gap={3}>
          <Dismissible onClose={deactivate} closeAriaLabel="Close modal">
            <Modal.Title>Modal title</Modal.Title>
            <Modal.Subtitle>Modal subtitle</Modal.Subtitle>
          </Dismissible>

          <Button onClick={() => {}}>Focusable button</Button>
          <TextField
            name="name"
            placeholder="Enter your name"
            inputAttributes={{ ref: inputRef }}
          />
        </View>
      </Modal>
    </>
  );
}

You can prevent focusing the first element and focus the whole Modal first by setting the autoFocus flag to false. In this case, the Modal is labeled by the Modal.Title and Modal.Subtitle, or you can pass the ariaLabel property to it.

<Modal autoFocus={false} ariaLabel="Country code selection" />

You can make the overlay transparent to keep the page content interactive while the Modal is open. When the Modal has a transparent overlay, it no longer locks the scroll.

function ExampleOverlay() {
  const { activate, deactivate, active } = useToggle(false);

  return (
    <>
      <Button onClick={activate}>Open side panel</Button>
      <Modal
        active={active}
        onClose={deactivate}
        position="end"
        transparentOverlay
      >
        Side panel content
      </Modal>
    </>
  );
}

Modal supports responsive syntax for the position, padding and size properties. Use object syntax to control their values based on the viewport size. Responsive properties are mobile-first, so selecting a value for a viewport will also apply it to all wider viewports.

In the following example, we're turning centered Modal into a bottom sheet for mobile screens:

<Modal position={{ s: 'bottom', l: 'center' }}>
  • Esc - close the Modal
  • Modal traps focus inside its root element, meaning that any type of keyboard navigation will keep the focus inside the Overlay while it's open.
  • Using Modal.Title and Modal.Subtitle automatically applies aria-labelledby and aria-describedby attributes to the dialog element.