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>
);
}
これでとりあえず開閉だけができる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>
);
}
CSSクラスでスタイリングする場合、 overlay
はReactModal__Overlay
、 content
はReactModal__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>
);
}
クリックしても上手く閉じない時は ReactModal__Content
に指定したスタイルを確認してみてください。 onRequestClose
に指定した関数が追加されるのは、あくまでも ReactModal__Overlay
要素なので、例えばReactModal__Content
に width: 100vw; height: 100vh;
のように画面いっぱいに広がるようなスタイルが適用されていると ReactModal__Overlay
がクリックできないため、思うように動作しません。試しにChrome DevToolsでReactModal__Overlay
を選択してから $0.click()
をコンソールで実行してみてください。onRequestClose
が正常に設定されていればこれで閉じるはずです。
react-modalにAnimationを追加する
react-modalにアニメーションを追加するには以下の3つの作業が必要です。
- overlayClassName, classNameにアニメーションをスタイリングするクラス名を指定する
- CSSでoverlayClassName, classNameで指定したクラス名を使ってスタイリングする
- 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;
}
上手く動かない場合、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>
);
}
コメント