モーダル・ダイアログの制御

モーダル・ダイアログは、ユーザーの操作を一時的に遮断して情報や確認を表示するオーバーレイUI。確認ダイアログ・フォーム入力・画像プレビューなど、メインコンテンツとは別の文脈で操作させたい場面で使われる。

open / opened state で表示を制御する controlled パターンが主流。Mantine の useDisclosure・shadcn/ui の Dialog Trigger など、開閉状態の管理を抽象化したユーティリティが各ライブラリで提供されている。

主なバリエーション
  • 開閉制御(controlled / uncontrolled)
  • オーバーレイの有無とクリック時の挙動
  • 開閉アニメーションのカスタマイズ
  • ネスト(モーダル内モーダル)
  • フォーカストラップとキーボード操作
  • サイズバリアントとフルスクリーンモード

ライブラリ横断比較

機能MantineAnt Designshadcn/uiMUI
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(useDisclosureopen/close/toggle を提供)・shadcn/ui(DialogTrigger で状態管理不要の uncontrolled パターン)
  • 確認ダイアログを命令的に呼び出したい → Ant Design(Modal.confirm() でコンポーネントツリー外から呼び出し可能)
  • ネストしたモーダルを管理したい → Mantine(useModalsStack でスタック管理、closeAll で一括クローズ)
  • アクセシビリティ(フォーカストラップ)を確保したい → shadcn/ui(Radix UI が自動処理)・Mantine・MUI(いずれも内蔵)
  • サイズをレスポンシブに制御したい → MUI(maxWidthfullWidth prop)・shadcn/ui(Tailwind の max-w-* クラスで柔軟に対応)