レビュー済み·難易度: 中級·更新: 2026-04-17

React の createPortal でモーダル・トーストを DOM 外に描画する

createPortal を使い、モーダルやトーストを親コンポーネントの DOM 外(body 直下)に描画する実装例。

nextjsui-component

対応バージョン

nextjs 15react 19

前提環境

React の useEffect と useRef の基本を理解していること

概要

createPortal を使うと、React ツリー上では子コンポーネントだが、DOM 上では別の要素(通常 body 直下)に描画できる。 overflow: hiddenz-index の影響を受けずにモーダル・トーストを表示したいときに使う。

インストール

# 追加インストールは不要

モーダルの実装例

// src/components/Modal.tsx
"use client";

import { createPortal } from "react-dom";
import { useEffect, useState } from "react";

type Props = {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
};

export function Modal({ isOpen, onClose, children }: Props) {
  const [mounted, setMounted] = useState(false);

  // SSR では document が存在しないため、mount 後にのみ Portal を描画する
  useEffect(() => {
    setMounted(true);
    return () => setMounted(false);
  }, []);

  if (!mounted || !isOpen) return null;

  return createPortal(
    <div
      className="fixed inset-0 z-50 flex items-center justify-center"
      role="dialog"
      aria-modal="true"
    >
      {/* オーバーレイ */}
      <div className="absolute inset-0 bg-black/50" onClick={onClose} />
      {/* コンテンツ */}
      <div className="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
        <button
          onClick={onClose}
          className="absolute right-4 top-4 text-gray-400 hover:text-gray-600"
          aria-label="閉じる"
        >
          ✕
        </button>
        {children}
      </div>
    </div>,
    document.body
  );
}
// src/app/page.tsx
"use client";

import { useState } from "react";
import { Modal } from "@/components/Modal";

export default function HomePage() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <main className="p-6">
      <button
        onClick={() => setIsOpen(true)}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
      >
        モーダルを開く
      </button>

      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <h2 className="mb-2 text-lg font-bold">タイトル</h2>
        <p className="text-sm text-gray-600">モーダルのコンテンツをここに書く。</p>
      </Modal>
    </main>
  );
}

ポイント

  • createPortal(children, container) の第2引数にレンダリング先の DOM 要素を渡す
  • SSR 対応のため useEffectmounted フラグを管理し、クライアントのみで Portal を描画する
  • イベント(onClick 等)は React ツリーに沿って伝播するため、Portal 先の DOM 階層とは無関係にバブリングする
  • Radix UI や headlessui が内部で createPortal を使っているため、外部ライブラリで十分な場合はそちらを使う
  • role="dialog"aria-modal="true" でアクセシビリティを確保する

注意点

createPortal は Radix UI 等のヘッドレス UI でも内部で使われている。z-index の重なり問題や overflow: hidden によるクリッピングを避けるために body 直下に描画する。SSR 環境では document が存在しないため useEffect 内で mounted フラグを使うこと。

関連サンプル