モーダル・ダイアログの制御
モーダル・ダイアログは、ユーザーの操作を一時的に遮断して情報や確認を表示するオーバーレイUI。確認ダイアログ・フォーム入力・画像プレビューなど、メインコンテンツとは別の文脈で操作させたい場面で使われる。
open / opened state で表示を制御する controlled パターンが主流。Mantine の useDisclosure・shadcn/ui の Dialog Trigger など、開閉状態の管理を抽象化したユーティリティが各ライブラリで提供されている。
主なバリエーション
- •開閉制御(controlled / uncontrolled)
- •オーバーレイの有無とクリック時の挙動
- •開閉アニメーションのカスタマイズ
- •ネスト(モーダル内モーダル)
- •フォーカストラップとキーボード操作
- •サイズバリアントとフルスクリーンモード
ライブラリ横断比較
| 機能 | Mantine | Ant Design | shadcn/ui | MUI |
|---|---|---|---|---|
| controlled / uncontrolled | ○ opened prop | ○ open prop | ○ open prop | ○ open prop |
| オーバーレイクリックで閉じる | ○ closeOnClickOutside | ○ maskClosable | ○ onInteractOutside | ○ onClose reason |
| 開閉アニメーション | ○ transitionProps | ○ styles.mask | ○ data-state | ○ TransitionComponent |
| ネスト対応 | ○ useModalsStack | △ zIndex管理 | △ 手動管理 | △ zIndex管理 |
| フォーカストラップ | ○ FocusTrap内蔵 | ○ 内蔵 | ○ Radix UI内蔵 | ○ 内蔵 |
| サイズバリアント | ○ size prop | △ width prop | △ max-w-*クラス | ○ maxWidth prop |
| フルスクリーンモード | ○ fullScreen prop | △ style実装 | △ classNameで実装 | ○ fullScreen prop |
○ = 対応 △ = 部分対応・制限あり × = 非対応
ライブラリ別コード例
各ライブラリでモーダル・ダイアログを実装する際の設定部分を抜粋しています。 動くデモは各比較ページでご確認ください。
Mantine
// useDisclosure で開閉状態を管理
import { Modal, Button } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function MyModal() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>開く</Button>
<Modal
opened={opened}
onClose={close}
title="モーダルタイトル"
size="md" // xs / sm / md / lg / xl / full
centered // 画面中央に配置
closeOnClickOutside // オーバーレイクリックで閉じる(デフォルト: true)
closeOnEscape // Esc キーで閉じる(デフォルト: true)
transitionProps={{ transition: 'fade', duration: 300 }}
trapFocus // フォーカストラップ(デフォルト: true)
>
<p>モーダルのコンテンツ</p>
<Button onClick={close}>閉じる</Button>
</Modal>
</>
);
}
// useModalsStack でネストしたモーダルを管理
import { useModalsStack } from '@mantine/core';
function NestedModals() {
const stack = useModalsStack(['first', 'second']);
return (
<>
<Button onClick={() => stack.open('first')}>最初のモーダルを開く</Button>
<Modal {...stack.register('first')} title="最初のモーダル">
<Button onClick={() => stack.open('second')}>次を開く</Button>
</Modal>
<Modal {...stack.register('second')} title="ネストしたモーダル">
<Button onClick={() => stack.closeAll()}>すべて閉じる</Button>
</Modal>
</>
);
}Ant Design
// controlled パターン
import { Modal, Button } from 'antd';
import { useState } from 'react';
function MyModal() {
const [open, setOpen] = useState(false);
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>開く</Button>
<Modal
title="モーダルタイトル"
open={open}
onOk={() => setOpen(false)}
onCancel={() => setOpen(false)}
maskClosable // オーバーレイクリックで閉じる(デフォルト: true)
centered // 画面中央
width={520} // 幅を指定(px)
// footer={null} フッターを非表示にする場合
>
<p>モーダルのコンテンツ</p>
</Modal>
</>
);
}
// Modal.confirm で確認ダイアログ(命令的API)
import { ExclamationCircleFilled } from '@ant-design/icons';
Modal.confirm({
title: '本当に削除しますか?',
icon: <ExclamationCircleFilled />,
content: 'この操作は取り消せません。',
onOk() { /* 削除処理 */ },
});shadcn/ui
// Dialog(Radix UI ベース)
import {
Dialog, DialogContent, DialogDescription,
DialogHeader, DialogTitle, DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
// uncontrolled パターン(Trigger で開閉)
function MyDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>開く</Button>
</DialogTrigger>
<DialogContent
className="max-w-md" // サイズは Tailwind で制御
// onInteractOutside で外側クリックの挙動をカスタマイズ
onInteractOutside={(e) => e.preventDefault()} // 閉じさせない場合
>
<DialogHeader>
<DialogTitle>モーダルタイトル</DialogTitle>
<DialogDescription>補足説明テキスト</DialogDescription>
</DialogHeader>
<p>コンテンツ</p>
</DialogContent>
</Dialog>
);
}
// controlled パターン
const [open, setOpen] = useState(false);
<Dialog open={open} onOpenChange={setOpen}>
{/* フォーカストラップは Radix UI が自動で処理 */}
<DialogContent>...</DialogContent>
</Dialog>MUI
// Dialog コンポーネント
import {
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
Button,
} from '@mui/material';
import { useState } from 'react';
function MyDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>開く</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
// onClose={(_, reason) => {
// if (reason !== 'backdropClick') setOpen(false); // 背景クリックで閉じない
// }}
maxWidth="sm" // xs / sm / md / lg / xl / false
fullWidth // maxWidth まで広げる
fullScreen={false} // true でフルスクリーン
>
<DialogTitle>モーダルタイトル</DialogTitle>
<DialogContent>
<DialogContentText>本当に削除しますか?</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>キャンセル</Button>
<Button onClick={() => setOpen(false)} variant="contained" color="error">
削除
</Button>
</DialogActions>
</Dialog>
</>
);
}まとめ・選び方のヒント
- •開閉状態の管理を楽にしたい → Mantine(
useDisclosureでopen/close/toggleを提供)・shadcn/ui(DialogTriggerで状態管理不要の uncontrolled パターン) - •確認ダイアログを命令的に呼び出したい → Ant Design(
Modal.confirm()でコンポーネントツリー外から呼び出し可能) - •ネストしたモーダルを管理したい → Mantine(
useModalsStackでスタック管理、closeAllで一括クローズ) - •アクセシビリティ(フォーカストラップ)を確保したい → shadcn/ui(Radix UI が自動処理)・Mantine・MUI(いずれも内蔵)
- •サイズをレスポンシブに制御したい → MUI(
maxWidth・fullWidthprop)・shadcn/ui(Tailwind のmax-w-*クラスで柔軟に対応)