0Pricing
Frontend Academy · レッスン

モーダルとツールチップのためのPortal

ReactDOM.createPortalを使って子要素を別のDOMノードにレンダリングし、overflow:hiddenのコンテナから抜け出すモーダルやツールチップを実現します。

「モーダルとツールチップのためのPortal」はCoddyKit上の無料Frontend Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはFrontend Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Frontend Academyコースには全4レッスンが含まれています。

コンテナから逃れる問題

モーダルやツールチップは、親のoverflow:hidden、z-indexの重なり順、transformによって作られたコンテキストから逃れ、DOMの最上位に描画する必要があります。解決策がポータルです。

createPortal API

createPortal(children, container)は、childrenをcontainer(任意のDOMノード)に描画します。同時に、イベントやContextの観点ではReactコンポーネントの論理的な子として扱われます。

import { createPortal } from 'react-dom';

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

ポータルのコンテナ

一般的な描画先は、document.body、index.html内の専用の#modal-root div、または必要に応じて作成したref付きのコンテナです。

<!-- index.html -->
<div id="root"></div>
<div id="modal-root"></div>

// component:
const modalRoot = document.getElementById('modal-root');
return createPortal(<div>...</div>, modalRoot);

インラインで描画するだけではいけない理由

親にoverflow: hiddenがあると、子のモーダルは切り取られます。親にtransform: scaleがあると、モーダルの固定配置が親を基準にしたものになります。ポータルを使えば、これらを回避できます。

イベントは引き続きバブルする

DOM内の別の場所に描画されても、ReactのイベントはDOMツリーではなくコンポーネントツリーを通じてバブルします。ポータル内でのクリックも、祖先にあるReactコンポーネントのonClickハンドラーまでバブルします。

オーバーレイのクリックで閉じる

モーダルのコンテンツでイベントの伝播を停止し、内部のクリックでモーダルが閉じないようにします。

function Modal({ onClose, children }) {
  return createPortal(
    <div className="backdrop" onClick={onClose}>
      <div className="content" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

Escapeキーで閉じる

モーダルが開いている間だけkeydownリスナーを追加し、アンマウント時に削除します。

useEffect(() => {
  const onKey = (e) => { if (e.key === 'Escape') onClose(); };
  window.addEventListener('keydown', onKey);
  return () => window.removeEventListener('keydown', onKey);
}, [onClose]);

ポータルでのフォーカス管理

モーダルを開いたときは、フォーカスを内部に移動し、閉じるまでフォーカスをトラップしてから、モーダルを開いた要素にフォーカスを戻します。これを自前で実装しないよう、focus-trap-reactのようなライブラリを使用してください。

ネイティブのdialog要素

HTMLの<dialog>は、モーダルの配置、バックドロップ、フォーカストラップ、Escapeによるクローズをネイティブで処理します。多くのモーダルでは、ポータルよりも優れた出発点になります。

function Modal({ open, onClose, children }) {
  const ref = useRef(null);
  useEffect(() => {
    if (open) ref.current?.showModal();
    else ref.current?.close();
  }, [open]);
  return (
    <dialog ref={ref} onClose={onClose}>
      {children}
    </dialog>
  );
}

ツールチップとポップオーバー

ツールチップはアンカー要素を基準に配置します。ポータルを使うと、重なり順のコンテキストから逃れられます。衝突検出、反転、配置にはfloating-uiを組み合わせます。

import { useFloating, autoUpdate } from '@floating-ui/react';

function Tooltip({ children, content }) {
  const { refs, floatingStyles } = useFloating({ whileElementsMounted: autoUpdate });
  const [open, setOpen] = useState(false);
  return (
    <>
      <button ref={refs.setReference} onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
        {children}
      </button>
      {open && createPortal(
        <div ref={refs.setFloating} style={floatingStyles}>{content}</div>,
        document.body
      )}
    </>
  );
}

SSRに関する注意点

サーバー上にはdocumentが存在しません。クライアントでのみ描画する(typeof windowを確認する)か、Next.jsのdynamicでssr:falseを使用してください。

アクセシビリティ:aria-modal

モーダルのコンテナには、role="dialog"、aria-modal="true"、モーダルの見出しを指すaria-labelledbyを設定してください。または、ネイティブの<dialog>を使用してください。

クイックチェック

モーダルをコンポーネントツリー内にインラインで描画するのではなく、createPortalを使うのはなぜですか?

復習:ポータル

createPortal(children, container)はDOM上の別の場所に描画しながら、Reactツリーでは論理的な関係を維持します。overflow、transform、z-indexから逃れる必要がある問題を解決できます。イベントは引き続きReactツリーを通じてバブルします。Escapeによるクローズとフォーカス管理を追加します。ネイティブの<dialog>は、モーダルに必要な処理の多くを行います。ツールチップにはfloating-uiを組み合わせます。SSR(typeof windowの確認)も忘れないでください。

よくある質問

「モーダルとツールチップのためのPortal」レッスンは無料ですか?

はい。「モーダルとツールチップのためのPortal」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Frontend Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Frontend Academyコースには全4レッスンが含まれています。

「モーダルとツールチップのためのPortal」で何を学びますか?

ReactDOM.createPortalを使って子要素を別のDOMノードにレンダリングし、overflow:hiddenのコンテナから抜け出すモーダルやツールチップを実現します。 ブラウザで直接実行するハンズオンコードでFrontend Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Frontend Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのFrontend Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「モーダルとツールチップのためのPortal」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このFrontend Academyレッスンでコードを書いて実行できますか?

はい。すべてのFrontend Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Contextを使った複合コンポーネント
  2. Render PropsとHOCパターン
  3. モーダルとツールチップのためのPortal
  4. エラーバウンダリー
← Frontend Academyに戻る