Back to React Spectrum

Modal

packages/react-aria-components/docs/Modal.mdx

2022-12-1615.6 KB
Original Source

{/* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}

import {Layout} from '@react-spectrum/docs'; export default Layout;

import docs from 'docs:react-aria-components'; import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from '/packages/react-aria/docs/overlays/modal-anatomy.svg'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import {Divider} from '@react-spectrum/divider'; import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard'; import {ExampleList} from '@react-spectrum/docs/src/ExampleList'; import {Keyboard} from '@react-spectrum/text'; import {StarterKits} from '@react-spectrum/docs/src/StarterKits';


category: Overlays keywords: [dialog, popover, aria] type: component

Modal

<PageDescription>{docs.exports.Modal.description}</PageDescription>

<HeaderInfo packageData={packageData} componentNames={['Modal']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/'} ]} />

Example

tsx
import {DialogTrigger, Modal, Dialog, Button, Heading, TextField, Label, Input} from 'react-aria-components';

<DialogTrigger>
  <Button>Sign up…</Button>
  <Modal>
    <Dialog>
      <form>
        <Heading slot="title">Sign up</Heading>
        <TextField autoFocus>
          <Label>First Name: </Label>
          <Input />
        </TextField>
        <TextField>
          <Label>Last Name: </Label>
          <Input />
        </TextField>
        <Button slot="close">
          Submit
        </Button>
      </form>
    </Dialog>
  </Modal>
</DialogTrigger>
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary> ```css hidden @import './Button.mdx' layer(button); @import './TextField.mdx' layer(textfield); @import './Dialog.mdx' layer(dialog); ```
css
@import "@react-aria/example-theme";

.react-aria-ModalOverlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: var(--page-height);
  background: rgba(0 0 0 / .5);
  z-index: 100;

  &[data-entering] {
    animation: modal-fade 200ms;
  }

  &[data-exiting] {
    animation: modal-fade 150ms reverse ease-in;
  }
}

.react-aria-Modal {
  position: fixed;
  max-height: var(--visual-viewport-height);
  top: calc(var(--visual-viewport-height) / 2);
  left: 50%;
  translate: -50% -50%;
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--overlay-background);
  color: var(--text-color);
  border: 1px solid var(--gray-400);
  outline: none;
  width: max-content;
  max-width: 300px;

  &[data-entering] {
    animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
  }

  .react-aria-TextField {
    margin-bottom: 8px;
  }
}

@keyframes modal-fade {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes modal-zoom {
  from {
    transform: scale(0.8);
  }

  to {
    transform: scale(1);
  }
}
</details>

Features

The HTML <dialog> element can be used to build modals. However, it is not yet widely supported across browsers, and building fully accessible custom dialogs from scratch is very difficult and error prone. Modal helps achieve accessible modals that can be styled as needed.

  • Styleable – States for entry and exit animations are included for easy styling. Both the underlay and overlay elements can be customized.
  • Accessible – Content outside the model is hidden from assistive technologies while it is open. The modal optionally closes when interacting outside, or pressing the <Keyboard>Escape</Keyboard> key.
  • Focus management – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside.
  • Scroll locking – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers.

Note: Modal only provides the overlay itself. It should be combined with Dialog to create fully accessible modal dialogs. Other overlays such as menus may also be placed in a modal overlay.

Anatomy

<Anatomy />

A modal consists of an overlay container element, and an underlay. The overlay may contain a Dialog, or another element such as a Menu or ListBox when used within a component such as a Select or ComboBox. The underlay is typically a partially transparent element that covers the rest of the screen behind the overlay, and prevents the user from interacting with the elements behind it.

tsx
import {Modal, ModalOverlay} from 'react-aria-components';

<ModalOverlay>
  <Modal />
</ModalOverlay>

Examples

<ExampleList tag="modal" />

Starter kits

To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library.

<StarterKits component="modal" tailwindComponent="alertdialog" />

Interactions

Dismissable

If your modal doesn't require the user to make a confirmation, you can set isDismissable on the Modal. This allows the user to click outside to close the dialog.

tsx
<DialogTrigger>
  <Button>Open dialog</Button>
  <Modal isDismissable>
    <Dialog>
      <Heading slot="title">Notice</Heading>
      <p>Click outside to close this dialog.</p>
    </Dialog>
  </Modal>
</DialogTrigger>

Keyboard dismiss disabled

By default, modals can be closed by pressing the <Keyboard>Escape</Keyboard> key. This can be disabled with the isKeyboardDismissDisabled prop.

tsx
<DialogTrigger>
  <Button>Open dialog</Button>
  <Modal isKeyboardDismissDisabled>
    <Dialog>
      <Heading slot="title">Notice</Heading>
      <p>You must close this dialog using the button below.</p>
      <Button slot="close">Close</Button>
    </Dialog>
  </Modal>
</DialogTrigger>

Custom overlay

ModalOverlay can be used to customize the backdrop rendered behind a Modal. Together with support for custom entry and exit animations, you can build other types of overlays beyond traditional modal dialogs such as trays or drawers.

tsx
import {ModalOverlay} from 'react-aria-components';

<DialogTrigger>
  <Button>Open modal</Button>
  <ModalOverlay className="my-overlay">
    <Modal className="my-modal">
      <Dialog>
        <Heading slot="title">Notice</Heading>
        <p>This is a modal with a custom modal overlay.</p>
        <Button slot="close">Close</Button>
      </Dialog>
    </Modal>
  </ModalOverlay>
</DialogTrigger>
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
css
.my-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: var(--page-height);
  background: rgba(45 0 0 / .3);
  backdrop-filter: blur(10px);

  &[data-entering] {
    animation: mymodal-blur 300ms;
  }

  &[data-exiting] {
    animation: mymodal-blur 300ms reverse ease-in;
  }
}

.my-modal {
  position: sticky;
  left: 0;
  width: 300px;
  /* Extra padding to account for iOS floating browser UI. */
  top: -100px;
  height: calc(100dvh + 200px);
  padding: 100px 0;
  margin-left: auto;
  background: var(--overlay-background);
  outline: none;
  border-left: 1px solid var(--border-color);
  box-shadow: -8px 0 20px rgba(0 0 0 / 0.1);

  &[data-entering] {
    animation: mymodal-slide 300ms;
  }

  &[data-exiting] {
    animation: mymodal-slide 300ms reverse ease-in;
  }
}

@keyframes mymodal-blur {
  from {
    background: rgba(45 0 0 / 0);
    backdrop-filter: blur(0);
  }

  to {
    background: rgba(45 0 0 / .3);
    backdrop-filter: blur(10px);
  }
}

@keyframes mymodal-slide {
  from {
    transform: translateX(100%);
  }

  to {
    transform: translateX(0);
  }
}
</details>

Controlled open state

The above examples have shown Modal used within a <DialogTrigger>, which handles opening the modal when a button is clicked. This is convenient, but there are cases where you want to show a modal programmatically rather than as a result of a user action, or render the <Modal> in a different part of the JSX tree.

To do this, you can manage the modal's isOpen state yourself and provide it as a prop to the <Modal> element. The onOpenChange prop will be called when the user closes the modal, and should be used to update your state.

tsx
function Example() {
  let [isOpen, setOpen] = React.useState(false);

  return (
    <>
      <Button onPress={() => setOpen(true)}>Open dialog</Button>
      <Modal isDismissable isOpen={isOpen} onOpenChange={setOpen}>
        <Dialog>
          <Heading slot="title">Notice</Heading>
          <p>Click outside to close this dialog.</p>
        </Dialog>
      </Modal>
    </>
  );
}

Custom trigger

DialogTrigger works out of the box with any pressable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the <Pressable> component, or using the usePress hook.

tsx
import {Pressable} from 'react-aria-components';

<DialogTrigger>
  <Pressable>
    <span role="button">Custom trigger</span>
  </Pressable>
  <Modal>
    <Dialog>
      <Heading slot="title">Dialog</Heading>
      <p>This dialog was triggered by a custom button.</p>
      <Button slot="close">Close</Button>
    </Dialog>
  </Modal>
</DialogTrigger>

Note that any <Pressable> child must have an interactive ARIA role or use an appropriate semantic HTML element so that screen readers can announce the trigger. Trigger components must forward their ref and spread all props to a DOM element.

tsx
const CustomTrigger = React.forwardRef((props, ref) => (
  <button {...props} ref={ref} />
));

Props

<PropTable component={docs.exports.Modal} links={docs.links} />

Styling

React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

css
.react-aria-Modal {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

jsx
<Modal className="my-modal">
</Modal>

In addition, modals support entry and exit animations, which are exposed as states using DOM attributes that you can target with CSS selectors. Modal and ModalOverlay will automatically wait for any exit animations to complete before removing the element from the DOM. See the animation guide for more details.

css
.react-aria-Modal[data-entering] {
  animation: slide 300ms;
}

.react-aria-Modal[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

jsx
<Modal className={({isEntering}) => isEntering ? 'slide-in' : ''}>
</Modal>

The states, selectors, and render props for each component used in a Modal are documented below.

A Modal can be targeted with the .react-aria-Modal CSS selector, or by overriding with a custom className. It supports the following states and render props:

<StateTable properties={docs.exports.ModalRenderProps.properties} />

ModalOverlay

By default, Modal includes a builtin ModalOverlay, which renders a backdrop over the page when a modal is open. This can be targeted using the .react-aria-ModalOverlay CSS selector. To customize the ModalOverlay with a different class name or other attributes, render a ModalOverlay and place a Modal inside.

The --page-height and --visual-viewport-height CSS custom property will be set on the ModalOverlay, the latter of which you can use to set the height of the Modal to account for the virtual keyboard on mobile.

css
.react-aria-ModalOverlay {
  position: absolute;
  height: var(--page-height);
}

.react-aria-Modal {
  /* Center modal without adding a extra wrapping div. */
  position: fixed;
  max-height: var(--visual-viewport-height);
  top: calc(var(--visual-viewport-height) / 2);
  left: 50%;
  translate: -50% -50%;
}

Advanced customization

Contexts

All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps).

<ContextTable components={['Modal']} docs={docs} />

This example shows a KeyboardModalTrigger component that shows a modal when a user presses a specific key from anywhere on the page. It uses ModalContext to set the open state of the nested modal.

tsx
import {ModalContext} from 'react-aria-components';

interface KeyboardModalTriggerProps {
  keyboardShortcut: string,
  children: React.ReactNode
}

function KeyboardModalTrigger(props: KeyboardModalTriggerProps) {
  let [isOpen, setOpen] = React.useState(false);
  React.useEffect(() => {
    let onKeyDown = (e: KeyboardEvent) => {
      if (e.key === props.keyboardShortcut) {
        setOpen(true);
      }
    };

    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [props.keyboardShortcut]);

  return (
    /*- begin highlight -*/
    <ModalContext.Provider value={{isOpen, onOpenChange: setOpen}}>
      {props.children}
    </ModalContext.Provider>
  );
}

The following example uses KeyboardModalTrigger to show a modal when the <Keyboard>/</Keyboard> key is pressed.

tsx
<KeyboardModalTrigger keyboardShortcut="/">
  <Modal isDismissable>
    <Dialog>
      <Heading slot="title">Command palette</Heading>
      <p>Your cool command palette UI here!</p>
    </Dialog>
  </Modal>
</KeyboardModalTrigger>

Hooks

If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useModalOverlay for more details.