Reactでモーダル実装|基本的なポップアップUIの作り方
Reactでモーダル(ポップアップ)を実装する基本的な方法をコード例付きで解説。useState、useEffect、Portalの使い方も学べます。
みなさん、こんなことありませんか?
「Reactでモーダル実装に挑戦したいけど、どうすればいいの?」 「ポップアップを作りたいけど、複雑で分からない」 「他のサイトみたいな、きれいなモーダルを作りたい」
Webアプリケーションを開発していると、ユーザーに確認を求めたり、詳細情報を表示したりするために、モーダル(ポップアップ)が必要になることが多くあります。 でも、初心者にとってモーダルの実装は意外と難しく感じられるものですよね。
大丈夫です! この記事では、Reactでモーダルを実装する基本的な方法から、実践的なカスタマイズまで、コード例を交えて詳しく解説します。
一緒に、使いやすいモーダルUIを作っていきましょう!
モーダルって何?
モーダルは、メインコンテンツの上に表示される「ダイアログボックス」のことです。 ユーザーがモーダルを閉じるまで、背景のコンテンツは操作できなくなります。
モーダルの基本的な特徴
モーダルには以下のような特徴があります。
- オーバーレイ表示: 既存のコンテンツの上に重ねて表示
- 背景のブロック: モーダルが開いている間は背景の操作を無効化
- 明確な閉じる手段: ×ボタンや背景クリックで閉じられる
- フォーカス管理: モーダル内に適切にフォーカスを移動
簡単に言うと、ユーザーの注意を特定のコンテンツに集中させるためのUIコンポーネントです。 よく見るのは、確認ダイアログや画像の拡大表示ですね。
基本的なモーダルを作ってみよう
まず、最もシンプルなモーダルを実装してみましょう。
基本的なモーダルコンポーネント
以下のコードは、基本的なモーダルの実装例です。
import React, { useState } from 'react';
import './Modal.css';
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
}
export default Modal;
このコードの流れを詳しく説明しますね。
if (!isOpen) return null
で、モーダルが閉じている時は何も表示しません。
これで、表示・非表示の制御ができます。
modal-overlay
は、画面全体を覆う半透明の背景です。
クリックするとonClose
が実行されて、モーダルが閉じます。
modal-content
は、実際のモーダルの内容です。
onClick={(e) => e.stopPropagation()}
で、モーダル内をクリックしても閉じないようにしています。
ヘッダー部分には、タイトルと閉じるボタン(×)があります。
ボディ部分には、children
で渡されたコンテンツが表示されます。
モーダルを使うコンポーネント
次に、作成したモーダルを実際に使ってみましょう。
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
return (
<div>
<h1>モーダル実装サンプル</h1>
<button onClick={openModal}>モーダルを開く</button>
<Modal
isOpen={isModalOpen}
onClose={closeModal}
title="サンプルモーダル"
>
<p>これはモーダルの内容です。</p>
<p>任意のコンテンツを表示できます。</p>
</Modal>
</div>
);
}
export default App;
使い方はとてもシンプルですね。
useState
でisModalOpen
の状態を管理しています。
true
の時にモーダルが表示され、false
の時に非表示になります。
openModal
とcloseModal
の関数で、状態を切り替えます。
ボタンをクリックするとopenModal
が実行されて、モーダルが表示されます。
Modal
コンポーネントに、必要なpropsを渡しています。
children
として渡したコンテンツが、モーダル内に表示されます。
CSSスタイリングの基本
モーダルを美しく表示するためには、適切なCSSスタイリングが必要です。
基本的なモーダルスタイル
モーダルのスタイリングの基本的な考え方を見てみましょう。
/* モーダルのオーバーレイ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
/* モーダルのコンテンツ */
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
/* モーダルのヘッダー */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.modal-close:hover {
color: #000;
}
/* モーダルのボディ */
.modal-body {
padding: 20px;
}
このCSSのポイントを説明しますね。
modal-overlayでは、position: fixed
で画面全体を覆います。
background-color: rgba(0, 0, 0, 0.5)
で、半透明の黒い背景を作ります。
Flexboxを使って、モーダルを画面の中央に配置しています。
z-index: 1000
で、他の要素より前面に表示されるようにしています。
modal-contentでは、白い背景に角丸とシャドウを付けて、モーダルらしい見た目にしています。
max-height: 80vh
で、画面の80%以下の高さに制限しています。
modal-headerでは、Flexboxでタイトルと閉じるボタンを両端に配置しています。 下に境界線を引いて、ヘッダーとボディを区別しています。
modal-closeでは、×ボタンのスタイルを設定しています。 ホバー時に色が変わることで、クリック可能だと分かりやすくしています。
React Portalを使った実装
より適切なモーダル実装には、React Portalを使用することをおすすめします。
Portal を使う理由
React Portalを使うことで、以下のメリットがあります。
- DOMの構造を最適化: モーダルを適切な位置に配置
- CSSの影響を回避: 親要素のスタイルに影響されない
- z-indexの問題を回避: より確実に最前面に表示
通常のReactコンポーネントは、親要素の中にレンダリングされます。 でも、モーダルは画面全体を覆うので、bodyの直下に配置する方が自然なんです。
Portal を使ったモーダル実装
React Portalを使った実装例を見てみましょう。
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, title, children }) {
const [modalRoot, setModalRoot] = useState(null);
useEffect(() => {
// モーダル用のDOM要素を作成
const root = document.createElement('div');
root.id = 'modal-root';
document.body.appendChild(root);
setModalRoot(root);
return () => {
// クリーンアップ処理
document.body.removeChild(root);
};
}, []);
if (!isOpen || !modalRoot) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
modalRoot
);
}
export default Modal;
Portalの仕組みを詳しく説明しますね。
useEffect
で、modal-root
というIDのdiv
要素を作成しています。
この要素をdocument.body
に追加して、モーダル専用の場所を確保します。
createPortal
で、モーダルのコンテンツをその場所にレンダリングします。
第1引数がレンダリングする内容、第2引数が配置先の要素です。
クリーンアップ処理で、作成した要素を削除しています。 これで、メモリリークを防げます。
if (!isOpen || !modalRoot) return null
で、モーダルが閉じている時や、まだ配置先が準備できていない時は何も表示しません。
このようにPortalを使用することで、より柔軟で安全なモーダル実装が可能になります。
キーボードイベントの処理
ユーザビリティを向上させるため、キーボードイベントの処理も実装しましょう。
Escキーでモーダルを閉じる
Escキーでモーダルを閉じる機能を追加してみましょう。
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, title, children }) {
const [modalRoot, setModalRoot] = useState(null);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
useEffect(() => {
const root = document.createElement('div');
root.id = 'modal-root';
document.body.appendChild(root);
setModalRoot(root);
return () => {
document.body.removeChild(root);
};
}, []);
if (!isOpen || !modalRoot) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
modalRoot
);
}
export default Modal;
キーボードイベント処理の仕組みを説明しますね。
handleKeyDown
関数で、押されたキーをチェックしています。
event.key === 'Escape'
で、Escキーが押されたかどうかを判定します。
Escキーが押されたら、onClose()
を実行してモーダルを閉じます。
document.addEventListener
で、キーボードイベントリスナーを追加しています。
isOpen
がtrue
の時だけ、イベントリスナーを有効にします。
クリーンアップ処理で、イベントリスナーを削除しています。 これで、メモリリークや意図しないイベント処理を防げます。
これにより、ユーザーがEscキーを押すことでモーダルを閉じることができます。 キーボード操作に慣れたユーザーには、とても便利な機能ですね。
フォーカス管理の実装
アクセシビリティを向上させるため、フォーカス管理も実装しましょう。
フォーカストラップの実装
モーダル内でフォーカスを適切に管理するためのコードです。
import React, { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, title, children }) {
const [modalRoot, setModalRoot] = useState(null);
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// モーダルが開いた時の処理
previousFocusRef.current = document.activeElement;
// モーダル内の最初のフォーカス可能な要素にフォーカス
setTimeout(() => {
if (modalRef.current) {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
}, 100);
}
return () => {
// モーダルが閉じた時に元の要素にフォーカスを戻す
if (previousFocusRef.current) {
previousFocusRef.current.focus();
}
};
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
useEffect(() => {
const root = document.createElement('div');
root.id = 'modal-root';
document.body.appendChild(root);
setModalRoot(root);
return () => {
document.body.removeChild(root);
};
}, []);
if (!isOpen || !modalRoot) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
ref={modalRef}
>
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
modalRoot
);
}
export default Modal;
フォーカス管理の仕組みを詳しく説明しますね。
modalRef
で、モーダルのコンテンツ要素への参照を保持しています。
previousFocusRef
で、モーダルを開く前にフォーカスがあった要素を記憶しています。
モーダルが開いた時の処理を見てみましょう。
まず、document.activeElement
で現在フォーカスがある要素を保存します。
次に、モーダル内のフォーカス可能な要素を検索します。 ボタン、リンク、入力フィールドなどが対象です。
見つかった最初の要素にfocus()
でフォーカスを移動します。
setTimeout
を使って、少し遅延させているのは、DOM更新を待つためです。
モーダルが閉じた時の処理では、保存しておいた元の要素にフォーカスを戻しています。 これで、キーボード操作がスムーズに続けられます。
このようなフォーカス管理により、モーダルのアクセシビリティが大幅に向上します。 特に、スクリーンリーダーを使うユーザーや、キーボードで操作するユーザーに優しい設計になります。
アニメーションの追加
モーダルにアニメーションを追加して、より滑らかな表示を実現しましょう。
CSS トランジションを使ったアニメーション
まず、CSSアニメーションを追加します。
/* アニメーション付きモーダル */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal-overlay.modal-open {
opacity: 1;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.modal-overlay.modal-open .modal-content {
transform: scale(1);
}
このCSSアニメーションの仕組みを説明しますね。
オーバーレイのフェードイン: opacity: 0
からopacity: 1
への変化で、背景がゆっくりと現れます。
transition: opacity 0.3s ease
で、0.3秒かけて滑らかに変化させています。
コンテンツのスケールアニメーション: transform: scale(0.9)
からtransform: scale(1)
への変化で、モーダルが少し小さい状態から通常サイズに拡大します。
これで、「ふわっと現れる」感じの演出ができます。
.modal-open
クラスが付いた時に、アニメーションが開始されます。
JavaScriptで、このクラスの付け外しを制御します。
アニメーション対応のモーダルコンポーネント
アニメーションに対応したモーダルの実装です。
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, title, children }) {
const [modalRoot, setModalRoot] = useState(null);
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (isOpen) {
setIsAnimating(true);
// アニメーション開始のため少し遅延
setTimeout(() => setIsAnimating(false), 10);
}
}, [isOpen]);
useEffect(() => {
const root = document.createElement('div');
root.id = 'modal-root';
document.body.appendChild(root);
setModalRoot(root);
return () => {
document.body.removeChild(root);
};
}, []);
if (!isOpen || !modalRoot) return null;
return createPortal(
<div
className={`modal-overlay ${!isAnimating ? 'modal-open' : ''}`}
onClick={onClose}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
modalRoot
);
}
export default Modal;
アニメーション制御の仕組みを説明しますね。
isAnimating
という状態で、アニメーションの開始タイミングを制御しています。
モーダルが開いた時、最初はisAnimating
がtrue
になります。
その後、setTimeout
で10ミリ秒後にisAnimating
をfalse
にします。
これで、少し遅延してからアニメーションが開始されます。
className={
modal-overlay ${!isAnimating ? 'modal-open' : ''}}
で、条件に応じてクラスを付けています。
isAnimating
がfalse
になると、modal-open
クラスが追加されて、CSSアニメーションが開始されます。
このように段階的にクラスを制御することで、滑らかなアニメーションを実現できます。
このようにアニメーションを追加することで、より洗練されたユーザー体験を提供できます。 ユーザーにとって、モーダルの開閉がより自然に感じられるようになりますね。
実用的なモーダルのバリエーション
実際のプロジェクトでは、用途に応じて様々なモーダルが必要になります。
確認ダイアログ
ユーザーに確認を求めるモーダルの実装例です。
function ConfirmModal({ isOpen, onClose, onConfirm, message }) {
const handleConfirm = () => {
onConfirm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="確認">
<p>{message}</p>
<div className="modal-buttons">
<button onClick={onClose} className="btn-cancel">
キャンセル
</button>
<button onClick={handleConfirm} className="btn-confirm">
確認
</button>
</div>
</Modal>
);
}
確認ダイアログの仕組みを説明しますね。
message
プロップで、表示するメッセージを受け取ります。
例えば、「本当に削除しますか?」などのテキストです。
onConfirm
プロップで、確認ボタンが押された時の処理を受け取ります。
handleConfirm
関数では、まずonConfirm()
を実行してから、onClose()
でモーダルを閉じます。
ボタンは2つあります。 「キャンセル」ボタンは、何もせずにモーダルを閉じるだけです。 「確認」ボタンは、処理を実行してからモーダルを閉じます。
フォーム入力モーダル
フォーム要素を含むモーダルの実装例です。
function FormModal({ isOpen, onClose, onSubmit }) {
const [formData, setFormData] = useState({
name: '',
email: ''
});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="情報入力">
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>名前:</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
required
/>
</div>
<div className="form-group">
<label>メールアドレス:</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
required
/>
</div>
<div className="modal-buttons">
<button type="button" onClick={onClose} className="btn-cancel">
キャンセル
</button>
<button type="submit" className="btn-submit">
送信
</button>
</div>
</form>
</Modal>
);
}
フォームモーダルの仕組みを説明しますね。
formData
という状態で、入力値を管理しています。
オブジェクト形式で、複数の入力項目をまとめて管理できます。
onChange
イベントで、入力値の変更を監視しています。
setFormData({...formData, name: e.target.value})
で、スプレッド演算子を使って既存のデータを保持しつつ、特定のフィールドだけを更新します。
handleSubmit
関数では、e.preventDefault()
でデフォルトのフォーム送信を防いでいます。
その後、onSubmit(formData)
で入力データを親コンポーネントに渡してから、モーダルを閉じます。
送信ボタンはtype="submit"
にして、Enterキーでも送信できるようにしています。
キャンセルボタンはtype="button"
にして、フォーム送信を防いでいます。
このように、用途に応じてモーダルをカスタマイズすることができます。 確認ダイアログやフォーム入力など、様々な場面で活用できますね。
モーダル実装のベストプラクティス
モーダルを実装する際に気をつけるべきポイントをまとめました。
パフォーマンス最適化
モーダルのパフォーマンスを最適化するためのポイントです。
- 条件付きレンダリング: 必要な時だけモーダルをレンダリング
- メモ化: React.memoを使用して不要な再レンダリングを防止
- 遅延読み込み: 重いコンテンツは必要になってから読み込み
条件付きレンダリングは、特に重要です。 モーダルが閉じている時は、DOM要素を作らないようにしましょう。
メモ化を使うことで、親コンポーネントが再レンダリングされても、モーダルは不要な更新を避けられます。
アクセシビリティ対応
アクセシビリティを考慮した実装のポイントです。
- 適切なARIAラベル: スクリーンリーダーへの対応
- フォーカス管理: キーボードナビゲーションの最適化
- 色のコントラスト: 視覚的にわかりやすい色使い
ARIAラベルを付けることで、スクリーンリーダーを使うユーザーにも内容が伝わりやすくなります。
例えば、aria-label="モーダルを閉じる"
をボタンに付けるなどです。
ユーザビリティ向上
ユーザビリティを向上させるためのポイントです。
- 明確な閉じる手段: 複数の方法でモーダルを閉じられる
- 適切なサイズ: コンテンツに応じた適切なサイズ設定
- レスポンシブ対応: 様々なデバイスサイズに対応
モーダルを閉じる方法は、複数用意しておくと親切です。 ×ボタン、背景クリック、Escキーなど、ユーザーの好みに応じて選べるようにしましょう。
心配いりません、これらのポイントを意識して実装すれば、使いやすいモーダルを作成できます。
よくある問題と解決方法
モーダル実装でよく遭遇する問題と解決方法をまとめました。
z-indexの問題
問題: モーダルが他の要素に隠れてしまう
解決方法:
.modal-overlay {
z-index: 9999; /* 十分に高い値を設定 */
}
z-indexの値は、サイト全体で使われている値を確認して設定しましょう。 通常、9999のような高い値を設定すれば、他の要素より前面に表示されます。
スクロール問題
問題: モーダルが開いている時に背景がスクロールできてしまう
解決方法:
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
この処理で、モーダルが開いている間は背景のスクロールを無効化できます。
overflow: 'hidden'
でスクロールを無効にし、overflow: 'unset'
で元に戻します。
クリーンアップ処理も忘れずに追加しましょう。 これで、モーダルが意図せず閉じた場合でも、スクロールが復活します。
モバイル対応
問題: モバイルデバイスでモーダルが適切に表示されない
解決方法:
@media (max-width: 768px) {
.modal-content {
width: 95%;
max-height: 90vh;
margin: 20px;
}
}
モバイルでは、画面サイズに応じてモーダルのサイズを調整しましょう。 幅は95%程度にして、画面の端に少し余白を残します。
高さも90vh程度に制限して、画面からはみ出さないようにします。 マージンを追加することで、画面の端にぴったりくっつかないようにしています。
これらの対策により、より安定したモーダル実装が可能になります。 問題が起きた時は、これらの解決方法を試してみてくださいね。
まとめ:使いやすいモーダルを作ろう
Reactでのモーダル実装について、基本的な方法から実践的なテクニックまで解説しました。
重要なポイント
- useState: モーダルの開閉状態を管理
- Portal: 適切なDOM構造でモーダルを表示
- イベント処理: キーボードイベントとクリックイベントの適切な処理
- アクセシビリティ: フォーカス管理とARIAラベルの実装
- スタイリング: 視覚的に美しく使いやすいデザイン
これらの要素を組み合わせることで、プロレベルのモーダルが作れます。
実装の段階
モーダル実装は以下の段階で進めることをおすすめします。
- 基本的なモーダル: 最小限の機能で動作確認
- スタイリング: CSSで見た目を整える
- Portal対応: より適切なDOM構造に変更
- アクセシビリティ: キーボード操作とフォーカス管理
- アニメーション: 滑らかな表示・非表示効果
- カスタマイズ: 用途に応じた機能追加
最初から完璧を目指さず、段階的に機能を追加していくのがコツです。 まずは動くものを作って、少しずつ改善していきましょう。
今日から始めよう
モーダルは多くのWebアプリケーションで必要になる重要なUIコンポーネントです。 基本的な実装から始めて、段階的に機能を追加していくことで、使いやすいモーダルを作成できます。
ぜひ、実際のプロジェクトでモーダル実装にチャレンジしてみてください。 きっと、「こんなに簡単にモーダルが作れるんだ!」と驚くはずです。
あなたも今日から、素敵なモーダルを作ってみませんか?