Skip to content

Dialog (Modal)

Dialog - DialogOverlay - DialogContent

An accessible dialog or "modal" window.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Open Dialog
      </button>

      <Dialog isOpen={state.showDialog}>
        <button
          className="close-button"
          onClick={() => setState({ showDialog: false })}
        >
          <VisuallyHidden>Close</VisuallyHidden>
          <span aria-hidden>×</span>
        </button>
        <p>Hello there. I am a dialog</p>
      </Dialog>
    </div>
  )}
</Component>

Installation

npm install @reach/dialog
# or
yarn add @reach/dialog

And then import the components you need:

import {
  Dialog,
  DialogOverlay,
  DialogContent
} from "@reach/dialog";

Dialog

High-level component to render a modal dialog window over the top of the page (or another dialog).

<Dialog>
  <p>Some Content</p>
</Dialog>

Dialog Props

PropType
element propsspread
isOpenbool
onDismissfunc
childrennode

Dialog element props

Type: spread

Any props not listed above will be spread onto the underlying DialogContent element, which in turn is spread onto the underlying div[data-reach-dialog-content].

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      <Dialog style={{ color: "red" }} isOpen={state.showDialog}>
        <p>My text is red because the style prop got applied to the div</p>
        <button onClick={() => setState({ showDialog: false })}>
          Okay
        </button>
      </Dialog>
    </div>
  )}
</Component>

Dialog isOpen

Type: bool default: true

Controls whether the dialog is open or not.

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

<Dialog isOpen={false}>
  <p>I will be closed</p>
</Dialog>

If you'd rather not have the dialog always rendered, you can put a guard in front of it and only render when it should be open. In this case you don’t need the isOpen prop at all.

Note, however, that the dialog will not render to the DOM when isOpen={false}, but you may want to save on the number of elements created in your render function. You should probably do this when your dialog contains a lot of elements.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      {state.showDialog && (
        <Dialog>
          <p>
            I don’t use <code>isOpen</code>, I just render when I should
            and not when I shouldn’t.
          </p>
          <button onClick={() => setState({ showDialog: false })}>
            Okay
          </button>
        </Dialog>
      )}
    </div>
  )}
</Component>

Dialog onDismiss

Type: func

When the user clicks outside the modal or hits the escape key, this function will be called. If you want the modal to close, you’ll need to set state.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      <Dialog
        isOpen={state.showDialog}
        onDismiss={() => setState({ showDialog: false })}
      >
        <p>
          It is your job to close this with state when the user
          clicks outside or presses escape.
        </p>
        <button onClick={() => setState({ showDialog: false })}>
          Okay
        </button>
      </Dialog>
    </div>
  )}
</Component>

Dialog children

Type: node

Accepts any renderable content.

<Dialog>
  <p>Anything you want, you got it.</p>
</Dialog>

DialogOverlay

Low-level component if you need more control over the styles or rendering of the dialog overlay.

Note: You must render a DialogContent inside.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      <DialogOverlay
        style={{ background: "hsla(0, 100%, 100%, 0.9)" }}
        isOpen={state.showDialog}
      >
        <DialogContent style={{ boxShadow: '0px 10px 50px hsla(0, 0%, 0%, 0.33)' }}>
          <p>The overlay styles are a white fade instead of the default black fade.</p>
          <button onClick={() => setState({ showDialog: false })}>
            Very nice.
          </button>
        </DialogContent>
      </DialogOverlay>
    </div>
  )}
</Component>

DialogOverlay CSS Selectors

Please see the styling guide.

Use the following CSS to target the overlay:

[data-reach-dialog-overlay] {
  background: hsla(0, 0%, 0%, 0.2);
}

DialogOverlay Props

PropType
element propsspread
isOpenbool
onDismissfunc
initialFocusRefref
childrennode

DialogOverlay element props

Type: spread

Any props not listed above will be spread onto the underlying div.

<DialogOverlay className="light-modal">
  <p>The underlying element will receive a class</p>
</DialogOverlay>

DialogOverlay isOpen

Type: bool

Same as Dialog isOpen

DialogOverlay onDismiss

Type: func

Same as Dialog onDismiss

DialogContent initialFocus

Type: ref

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

() => {
  class App extends React.Component {

    constructor() {
      super()
      this.buttonRef = React.createRef();
      this.state = { showDialog: false };
      this.open = () => this.setState({ showDialog: true });
      this.close = () => this.setState({ showDialog: false });
    }

    render() {
      return (
        <div>
          <button onClick={this.open}>Show Dialog</button>

          {this.state.showDialog && (
            <DialogOverlay initialFocusRef={this.buttonRef}>
              <DialogContent>
                <p>
                  Pass the button ref to DialogOverlay and
                  the button.
                </p>
                <button onClick={this.close}>Not me</button>{" "}
                <button
                  ref={this.buttonRef}
                  onClick={this.close}
                >
                  Got me!
                </button>
              </DialogContent>
            </DialogOverlay>
          )}
        </div>
      );
    }
  }

  return <App />;
};

DialogOverlay children

Type: node

Should be a DialogContent.

<DialogOverlay>
  <DialogContent>
    Hey!
  </DialogContent>
</DialogOverlay>

DialogContent

Low-level component if you need more control over the styles or rendering of the dialog content.

Note: Must be a child of DialogOverlay.

Note: You only need to use this when you are also styling DialogOverlay, otherwise you can use the high-level Dialog component and pass the props to it. Any props passed to Dialog component (besides isOpen and onDismiss) will be spread onto DialogContent.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      <DialogOverlay isOpen={state.showDialog}>
        <DialogContent
          style={{
            border: "solid 5px hsla(0, 0%, 0%, 0.5)",
            borderRadius: "10px"
          }}
        >
          <p>I have a nice border now.</p>
          <p>Note that we could have used the simpler <code>Dialog</code> instead.</p>
          <button onClick={() => setState({ showDialog: false })}>
            Got it.
          </button>
        </DialogContent>
      </DialogOverlay>
    </div>
  )}
</Component>

DialogContent CSS Selectors

Please see the styling guide.

Use the following CSS to target the overlay:

[data-reach-dialog-content] {
  border: solid 5px hsla(0, 0%, 0%, 0.5);
}

DialogContent Props

PropType
element propsspread
childrennode

DialogContent element props

Type: spread

Any props not listed above will be spread onto the underlying div.

<DialogContent className="nice-border">
  <p>The underlying element will receive a class</p>
</DialogOverlay>

DialogContent children

Type: node

Accepts any renderable content.

<DialogContent>
  <p>Anything you want, you got it.</p>
</DialogContent>

Animation Example

If you'd like to animate the content, give React Spring a shot.

<Component initialState={{ showDialog: false }}>
  {({ state, setState }) => (
    <div>
      <button onClick={() => setState({ showDialog: true })}>
        Show Dialog
      </button>

      <Transition
        from={{ opacity: 0, y: -10 }}
        enter={{ opacity: 1, y: 0 }}
        leave={{ opacity: 0, y: 10 }}
      >
        {state.showDialog &&
          (styles => (
            <DialogOverlay style={{ opacity: styles.opacity }}>
              <DialogContent
                style={{
                  transform: `translate3d(0px, ${styles.y}px, 0px)`,
                  border: "4px solid hsla(0, 0%, 0%, 0.5)",
                  borderRadius: 10
                }}
              >
                <button onClick={() => setState({ showDialog: false })}>
                  Close Dialog
                </button>
                <p>React Spring makes it too easy!</p>
                <button>Ayyyyyy</button>
              </DialogContent>
            </DialogOverlay>
          ))}
      </Transition>
    </div>
  )}
</Component>

Accessibility

Tabbable Elements

It is recommended to have at least one tabbable element in the dialog content. Ideally the first element in the dialog is a close button. If no tabbable elements are found, the dialog content element itself will receive focus.

Aria Hiding Other Elements

The aria role "dialog" has been problematic in the past with the virtual cursor, effectively hiding a lot of dialog content from screenreader users. Instead, Dialog will set aria-hidden on all nodes at the document.body root except for the currently active dialog. This traps the virtual cursor inside the dialog.

This is a little unusual for a React component to traverse the DOM that it isn't a part of. Care has been taken to restore the manipulated attributes back to their original values.

Keyboard Accessibility

keyaction
EscapeDismisses the dialog (if the app allows)

Z Index Wars

Dialog does not set a z-index, it depends on the DOM order to be on top of the rest of the app (the overlay is inserted at the end of the document when it is opened). If your app is already battling in the z-index wars, be sure to add a z-index to the <Dialog> or <DialogOverlay> that you render.