gladevise Logo
HomeAboutContact

react-modalを使ってModalコンポーネントを作る


react-modalを使って下の動画のようなアニメーション付きのModalコンポーネントを作る方法を説明します。この記事では以下のような特徴を持つコンポーネントを作ります。

  • モーダルをクリックするとCSSアニメーション付きでモーダルを開く
  • モーダル内のボタンか、モーダルの外側をクリックするとモーダルを閉じる
  • CSS-in-JS(Emotion)を使って、モーダルコンポーネントのCSSをコンポーネント内に閉じる(グローバルなCSSを使わない)

Install react-modal

まずは以下のコマンドでreact-modalをインストールしてください。

npm install --save react-modal

or

yarn add react-modal

react-modalの使い方

react-modalはModalコンポーネントをimportして使います。それとは別にModalの表示状態を保持するstateを定義する必要があります。クラスコンポーネントでも問題なく動作しますが、ここはより簡潔に書けるReact Hooksを使います。

最もシンプルなreact-modalの使い方

最もシンプルにreact-modalを使うには、hooksで状態を定義し、Modalを開くボタンと閉じるボタンを書き、ModalのisOpen propにhooksの状態を渡します。

Modal.setAppElement()でreactのマウント要素のセレクターを指定しておきます。こうすることで、Modalが開いた時にスクリーンリーダーが <main>を読んでしまうことを避けることが出来ます。詳しくはドキュメントをご覧ください。

import React from "react";
import "./styles.css";
import Modal from "react-modal";

Modal.setAppElement("#root");

export default function App() {
  const [modalIsOpen, setIsOpen] = React.useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={modalIsOpen}>
        <button onClick={() => setIsOpen(false)}>Close Modal</button>
      </Modal>
    </div>
  );
}

https://codesandbox.io/embed/react-modal-simple-0p3fg

これでとりあえず開閉だけができるModalが表示できます。

react-modalにstyle propを使ってスタイリングする

react-modalにスタイリングするには style propを使ってinline styleを追加する方法とCSSクラスを使ってスタイリングする方法の2種類があります。

例えば下はinline styleの例です。 overlayでModalの外側、 contentでModalの内側のスタイリングが出来ます。

import React from "react";
import "./styles.css";
import Modal from "react-modal";

Modal.setAppElement("#root");

const modalStyle = {
  overlay: {
    position: "fixed",
    top: 0,
    left: 0,
    backgroundColor: "rgba(0,0,0,0.85)"
  },
  content: {
    position: "absolute",
    top: "5rem",
    left: "5rem",
    right: "5rem",
    bottom: "5rem",
    backgroundColor: "paleturquoise",
    borderRadius: "1rem",
    padding: "1.5rem"
  }
};

export default function App() {
  const [modalIsOpen, setIsOpen] = React.useState(false);
  return (
    <div className="App">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={modalIsOpen} style={modalStyle}>
        <button onClick={() => setIsOpen(false)}>Close Modal</button>
      </Modal>
    </div>
  );
}

https://codesandbox.io/embed/react-inline-style-flu3u

CSSクラスでスタイリングする場合、 overlayReactModal__OverlaycontentReactModal__Contentにスタイリングします。

react-modalでModalの外側をクリックして閉じる方法

onRequestCloseに関数を加えることでModalの外をクリックするとModalを閉じるようにできます。

import React from "react";
import "./styles.css";
import Modal from "react-modal";

Modal.setAppElement("#root");

export default function App() {
  const [modalIsOpen, setIsOpen] = React.useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={modalIsOpen} onRequestClose={() => setIsOpen(false)}>
        <button onClick={() => setIsOpen(false)}>Close Modal</button>
      </Modal>
    </div>
  );
}

https://codesandbox.io/embed/react-modal-close-on-click-outer-modal-83wq9

クリックしても上手く閉じない時は ReactModal__Contentに指定したスタイルを確認してみてください。 onRequestCloseに指定した関数が追加されるのは、あくまでも ReactModal__Overlay要素なので、例えばReactModal__Contentwidth: 100vw; height: 100vh;のように画面いっぱいに広がるようなスタイルが適用されていると ReactModal__Overlayがクリックできないため、思うように動作しません。試しにChrome DevToolsでReactModal__Overlayを選択してから $0.click()をコンソールで実行してみてください。onRequestCloseが正常に設定されていればこれで閉じるはずです。

react-modalにAnimationを追加する

react-modalにアニメーションを追加するには以下の3つの作業が必要です。

  1. overlayClassName, classNameにアニメーションをスタイリングするクラス名を指定する
  2. CSSでoverlayClassName, classNameで指定したクラス名を使ってスタイリングする
  3. transition-durationに指定した時間を closeTimeoutMSに指定する

詳しくはドキュメントを確認してください。

以下はほんの一例です。modalの外側であるoverlayのスタイルはoverlayClassNameに設定します。 例えばbase: "overlay-base", afterOpen: "overlay-after", beforeClose: "overlay-before"のようにします。baseは最初にModalに適用されるスタイル、afterOpenはモーダルが開いた後に適用されるスタイル、beforeCloseはモーダルの閉じるボタンが押された後に適用されるスタイルです。modal内の要素であるcontentにも同じ要領でクラス名を定義します。

またアニメーションのtarandition-durationに設定する予定の時間を closeTimeoutMSに設定します。こうすることで閉じるボタンを押された瞬間に指定時間分だけ処理が止まるのでアニメーションを最後まで再生することが出来ます。

import React from "react";
import "./styles.css";
import Modal from "react-modal";

Modal.setAppElement("#root");

export default function App() {
  const [modalIsOpen, setIsOpen] = React.useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal
        isOpen={modalIsOpen}
        onRequestClose={() => setIsOpen(false)}
        overlayClassName={{
          base: "overlay-base",
          afterOpen: "overlay-after",
          beforeClose: "overlay-before"
        }}
        className={{
          base: "content-base",
          afterOpen: "content-after",
          beforeClose: "content-before"
        }}
        closeTimeoutMS={500}
      >
        <button onClick={() => setIsOpen(false)}>Close Modal</button>
      </Modal>
    </div>
  );
}

以下はスタイルの一例です。overlayにはbackground-colorとopacityを設定して、モーダルが開くと、徐々に不透明度が上がるようにしました。contentにはbackground-colorとwidth, heightを設定して、モーダルが開くとcontentが徐々に大きくなるようにしました。

.overlay-base {
  padding: 1rem;
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0);
  opacity: 0;
  transition-property: background-color, opacity;
  transition-duration: 500ms;
  transition-timing-function: ease-in-out;
  outline: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.overlay-after {
  background-color: rgba(0, 0, 0, 0.8);
  opacity: 1;
}

.overlay-before {
  background-color: rgba(0, 0, 0, 0);
  opacity: 0;
}

.content-base {
  position: relative;
  top: auto;
  left: auto;
  right: auto;
  bottom: auto;
  margin: 0 auto;
  border: 0;
  outline: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 0%;
  width: 0%;
  background-color: transparent;
  transition-property: background-color, width, height;
  transition-duration: 500ms;
  transition-timing-function: ease-in-out;
}

.content-after {
  width: 70%;
  height: 40%;
  background-color: rgba(250, 190, 190, 0.8);
}

.content-before {
  width: 0%;
  height: 0%;
  background-color: transparent;
}

https://codesandbox.io/embed/react-modal-animation-p2qnw

上手く動かない場合、Modalのstyle propを指定していないかどうか確認してみてください。style propに設定してしまうと、inline styleになってしまうので、いくらクラスを使ってスタイルを指定しても詳細度負けしてしまいます。

CSS-in-JS(Emotion)を使ってreact-modalのスタイルを書く

ここまでは style.cssを使ってグローバルなスタイルを適用していましたが、この方法の場合、複数のModalコンポーネントを使用した際にスタイルが衝突してしまいます。せっかくReactを使っているのですから、CSS-in-JSを使ってスコープのあるスタイリングをしたくなります。

ここではEmotionのClassNamesを使ってreact-modalにEmotionが自動生成するクラス名を渡したいと思います。以下のようにするとwrapperClassNameに color: green;を指定するクラスが代入されます。

import { ClassNames } from '@emotion/core'

// this might be a component from npm that accepts a wrapperClassName prop
let SomeComponent = props => (
  <div className={props.wrapperClassName}>
    in the wrapper!
    <div className={props.className}>{props.children}</div>
  </div>
)

render(
  <ClassNames>
    {({ css, cx }) => (
      <SomeComponent
        wrapperClassName={css({ color: 'green' })}
        className={css`
          color: hotpink;
        `}
      >
        from children!!
      </SomeComponent>
    )}
  </ClassNames>
)

詳しくはEmotionのドキュメントをご覧ください。

これを使ってreact-modalの portalClassNameにクラス名を指定します。portalClassName={css``}のようにしてスタイルを定義すればportalClassNameに定義したスタイルのクラス名が入ります。

import React from "react";
import { ClassNames } from "@emotion/core";
import Modal from "react-modal";

Modal.setAppElement("#root");

export default function App() {
  const [modalIsOpen, setIsOpen] = React.useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <ClassNames>
        {({ css, cx }) => (
          <Modal
            isOpen={modalIsOpen}
            onRequestClose={() => setIsOpen(false)}
            overlayClassName={{
              base: "overlay-base",
              afterOpen: "overlay-after",
              beforeClose: "overlay-before"
            }}
            className={{
              base: "content-base",
              afterOpen: "content-after",
              beforeClose: "content-before"
            }}
            closeTimeoutMS={500}
            portalClassName={css`
              .overlay-base {
                padding: 1rem;
                position: fixed;
                top: 0;
                bottom: 0;
                right: 0;
                left: 0;
                background-color: rgba(0, 0, 0, 0);
                opacity: 0;
                transition-property: background-color, opacity;
                transition-duration: 500ms;
                transition-timing-function: ease-in-out;
                outline: 0;
                display: flex;
                justify-content: center;
                align-items: center;
              }

              .overlay-after {
                background-color: rgba(0, 0, 0, 0.8);
                opacity: 1;
              }

              .overlay-before {
                background-color: rgba(0, 0, 0, 0);
                opacity: 0;
              }

              .content-base {
                position: relative;
                top: auto;
                left: auto;
                right: auto;
                bottom: auto;
                margin: 0 auto;
                border: 0;
                outline: 0;
                display: flex;
                justify-content: center;
                align-items: center;
                height: 0%;
                width: 0%;
                background-color: transparent;
                transition-property: background-color, width, height;
                transition-duration: 500ms;
                transition-timing-function: ease-in-out;
              }

              .content-after {
                width: 70%;
                height: 40%;
                background-color: rgba(250, 190, 190, 0.8);
              }

              .content-before {
                width: 0%;
                height: 0%;
                background-color: transparent;
              }
            `}
          >
            <button onClick={() => setIsOpen(false)}>Close Modal</button>
          </Modal>
        )}
      </ClassNames>
    </div>
  );
}

https://codesandbox.io/embed/react-modal-emotion-rk1k5

Recent Posts

Next.jsでsitemap.xml生成する

Next.jsでSSRやnext-sitemapを用いてsitemap.xmlを生成する方法を紹介します。

Rust製の爆速端末Alacrittyのインストールと設定方法

Rust製のGPUで高速レンダリングするターミナルエミュレータAlacrittyのインストール方法、tmux、NeoVimでのTrue Color、カラースキーム、Font、デフォルトの端末の設定などの紹介をします。

react-modalを使ってModalコンポーネントを作る

react-modalを使ってModalコンポーネントを作る方法を解説します。モーダルの外側をクリックして閉じる、アニメーションを付ける等の基本的な機能から、CSS-in-JS(Emotion)を使ってタイリングの方法を説明します。

Emotionを使ってGatsbyでtailwindを使う方法

Gatsby & Emotion & tailwindの環境構築、スタイリング方法についてまとめました。twin.macroを使えば、classNameだけでなく、Styled Componentsやcss propでスタイリングできます。

GatsbyのLinter/Formatter, CSS-in-JS環境構築

Gatsbyのインストール方法、プロジェクトの作り方、Linter(ESLint, stylelint)/Formatter(prettier)、CSS-in-JS(styled-components, Emotion)、tailwindの設定方法を説明します。