こんにちは、エキサイト株式会社の奥川です。
エキサイトホールディングス・アドベントカレンダー2024シリーズ2の17日目を担当します。
今回は、Next.jsでいい感じのモーダルを作成する方法をご紹介します。
完成形
コンポーネントと呼び出し側のコードは下記になります。スタイルは適当です。
モーダルコンポーネント
// Modal.tsx import { FC, ReactNode, useRef, useEffect } from 'react' import { createPortal } from 'react-dom' type ModalProps = { isOpen: boolean onClose: () => void children: ReactNode } const Modal: FC<ModalProps> = ({ isOpen, onClose, children }) => { const dialogRef = useRef<HTMLDialogElement>(null) useEffect(() => { if (isOpen) { // body要素にoverflow: hiddenを設定するとモーダル表示時に背景がスクロールしなくなる document.body.style.overflow = 'hidden' dialogRef.current?.showModal() } else { dialogRef.current?.close() } }, [isOpen]) if (!isOpen) return null const handleClose = () => { onClose() // body要素のoverflow: hiddenを戻す document.body.style.overflow = '' // ボタンのフォーカスを外す document.activeElement instanceof HTMLElement && document.activeElement.blur() } return ( <> {createPortal( <dialog> {children} <button onClick={handleClose} style={{ marginTop: '1rem', padding: '0.5rem 1rem', cursor: 'pointer' }} > 閉じる </button> </dialog>, document.body, )} </> ) } export default Modal
呼び出し部分の例
import { useState } from 'react' import Modal from './Modal' const ModalExample = () => { const [isOpen, setIsOpen] = useState(false) return ( <div> <h1>Modal表示の例</h1> <button onClick={() => seIsOpen(true)} style={{ padding: '0.5rem 1rem', cursor: 'pointer' }} > モーダルを開く </button> <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}> <h2>モーダルの中身</h2> <p>ここに任意のコンテンツを入れることができます。</p> </Modal> </div> ) }
モーダル作成時に意識した点
DOM上ではbody要素末尾だが、コード上ではコンポーネント内に書きたい
モーダルは次の観点からDOM上ではbody要素直下の末尾に記載されることが多いです。
- 全ての要素より優先して表示させたい
- 他の場所とスタイルを独立させたい
しかし、画面全体を複数コンポーネントで分けて実装している場合、コンポーネントの中でモーダルを書こうとするとbody要素の途中でモーダルを書くことになります。ReactではcreatPortalを利用することで、DOM上の別の場所にコードを移動させられるため、コンポーネントの中でモーダルを実装してもbodyの直下に配置させることができます。
dialog要素を活用する
モーダルはdialog要素を用いて実装します。
dialog要素を利用すると、backdropで背景のCSSを設定できたり、アクセシビリティ周りの設定をしなくてもブラウザがモーダルと認識してくれるため、非常に便利です。dialog要素を利用する場合、モーダルの開閉作業はHTMLDialogElement.showModal()
/close()
をそれぞれ利用します。ただ、Reactではコンポーネントを純粋に保つことが推奨されます。そのため、モーダルの開閉状態はpropsで管理し、実際の開閉処理はHTMLDialogElement.showModal()
/close()
を利用しています。
おわりに
今回はReactでモーダル用のコンポーネントを作成する方法を紹介しました。開閉ロジックをカスタムフックにしたり、まだ改善の余地はあるかと思います。まだ、dialog要素を利用する場合はcreatePortalでbody直下に飛ばさなくても問題なかったります。今回の記事が実装の助けになれば幸いです。