Reactで良い感じのモーダルを作成する

こんにちは、エキサイト株式会社の奥川です。

エキサイトホールディングス・アドベントカレンダー2024シリーズ2の17日目を担当します。

qiita.com

今回は、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の直下に配置させることができます。

ja.react.dev

dialog要素を活用する

モーダルはdialog要素を用いて実装します。

developer.mozilla.org

dialog要素を利用すると、backdropで背景のCSSを設定できたり、アクセシビリティ周りの設定をしなくてもブラウザがモーダルと認識してくれるため、非常に便利です。dialog要素を利用する場合、モーダルの開閉作業はHTMLDialogElement.showModal()/close()をそれぞれ利用します。ただ、Reactではコンポーネントを純粋に保つことが推奨されます。そのため、モーダルの開閉状態はpropsで管理し、実際の開閉処理はHTMLDialogElement.showModal()/close()を利用しています。

おわりに

今回はReactでモーダル用のコンポーネントを作成する方法を紹介しました。開閉ロジックをカスタムフックにしたり、まだ改善の余地はあるかと思います。まだ、dialog要素を利用する場合はcreatePortalでbody直下に飛ばさなくても問題なかったります。今回の記事が実装の助けになれば幸いです。