モーダルとツールチップのための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フィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Contextを使った複合コンポーネント
- Render PropsとHOCパターン
- モーダルとツールチップのためのPortal
- エラーバウンダリー