4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MUI モーダル系コンポーネント忘備録

Posted at

目次

  1. 各コンポーネントの特徴と比較
  2. Modal
  3. Dialog
  4. Menu
  5. Popover
  6. Drawer
  7. Tooltip

1. 比較

特徴 Modal Dialog Menu Popover Drawer Tooltip
背景オーバーレイ × ×
スクロールブロック × × ×
アンカー要素 不要 不要 必要 必要 不要 必要
表示位置 画面中央 画面中央 アンカー基準 アンカー基準 画面端 アンカー基準
主な用途 重要な操作 確認画面 ナビゲーション 詳細表示 サイドバー ヘルプテキスト
キーボード操作 Escで閉じる Tab/Esc 矢印キー Tab/Esc Tab/Esc なし
アニメーション Fade Fade/Slide Grow Grow Slide Fade

2. Modal

  • 複雑なフォームやコンテンツの表示
  • 重要な操作や確認が必要な場合
  • ユーザーの作業を意図的に中断させる場合
  • 画面中央に表示

❌ Bad

  • 軽微な確認での使用する
  • 小さな通知やアラート
  • 頻繁な表示/非表示の切り替え
  • 複数のModalを重ねて表示する

✅ Good

  • ユーザー登録フォーム等の複雑なフォームや大量の情報を表示する
  • Modalは一度に一つだけ表示する

実装例

import { useState } from 'react'
import { Modal, Box, Button, Typography } from '@mui/material'

export const SimpleModal = () => {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="contained" onClick={() => setOpen(true)}>
        モーダルを開く
      </Button>

      <Modal open={open} onClose={() => setOpen(false)}>
        <Box
          sx={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            bgcolor: 'background.paper',
            width: 400,
            p: 3,
          }}
        >
          <Typography>モーダルのタイトル</Typography>
          <Typography sx={{ mt: 2 }}>内容</Typography>
          <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
            <Button onClick={() => setOpen(false)}>閉じる</Button>
          </Box>
        </Box>
      </Modal>
    </>
  )
}

3. Dialog

  • シンプルな確認の際に使用する
  • 短いメッセージを表示
  • Yes/Noの選択を求める場合に使う
  • 画面中央に表示する

❌ Bad

  • 複雑なフォームの表示
  • 大量のコンテンツの表示
  • 長時間の作業が必要な操作

✅ Good

  • 削除の確認
  • 簡単な警告メッセージ
  • 単純な選択肢の提示

実装例

import { useState } from 'react'
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  Button,
  Box,
  Typography,
} from '@mui/material'

export const SimpleDialog = () => {
  const [open, setOpen] = useState(false)

  const handleConfirm = () => {
    // 削除するを押した時の処理
    setOpen(false)
  }

  return (
    <>
      <Button variant="contained" color="error" onClick={() => setOpen(true)}>
        アカウントを削除
      </Button>

      <Dialog open={open} onClose={() => setOpen(false)} fullWidth>
        <DialogTitle>
          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
            <Typography>アカウントの削除確認</Typography>
          </Box>
        </DialogTitle>

        <DialogContent>
          <DialogContentText>
            アカウントを削除すると、すべてのデータが完全に削除され、復元することはできません。
            この操作は取り消すことができません。
          </DialogContentText>
        </DialogContent>

        <DialogActions sx={{ p: 2 }}>
          <Button onClick={() => setOpen(false)} color="inherit">
            キャンセル
          </Button>
          <Button
            onClick={handleConfirm}
            variant="contained"
            color="error"
            autoFocus
          >
            削除する
          </Button>
        </DialogActions>
      </Dialog>
    </>
  )
}

4. Menu

  • シンプルなアクション一覧
  • 単一選択の操作

❌ Bad

  • 大量のコンテンツをMenuに詰め込む
  • フォーム要素をMenuに配置
  • 深いネストの階層構造

✅ Good

  • シンプルな選択肢を表示する
  • 大きなコンテンツはModalを使用
  • フォームはPopoverやModalを使用

実装例

import { useState } from 'react'
import {
  Menu,
  MenuItem,
  Button,
  ListItemIcon,
  ListItemText,
} from '@mui/material'
import {
  Settings as SettingsIcon,
  Person as PersonIcon,
  Logout as LogoutIcon,
} from '@mui/icons-material'

export const SimpleMenu = () => {
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)

  return (
    <>
      <Button variant="contained" onClick={(e) => setAnchorEl(e.currentTarget)}>
        メニューを開く
      </Button>

      <Menu
        anchorEl={anchorEl}
        open={Boolean(anchorEl)}
        onClose={() => setAnchorEl(null)}
      >
        <MenuItem onClick={() => setAnchorEl(null)}>
          <ListItemIcon>
            <PersonIcon fontSize="small" />
          </ListItemIcon>
          <ListItemText>プロフィール</ListItemText>
        </MenuItem>
        <MenuItem onClick={() => setAnchorEl(null)}>
          <ListItemIcon>
            <SettingsIcon fontSize="small" />
          </ListItemIcon>
          <ListItemText>設定</ListItemText>
        </MenuItem>
        <MenuItem onClick={() => setAnchorEl(null)}>
          <ListItemIcon>
            <LogoutIcon fontSize="small" />
          </ListItemIcon>
          <ListItemText>ログアウト</ListItemText>
        </MenuItem>
      </Menu>
    </>
  )
}

5. Popover

  • 表示位置を指定する
  • カスタマイズ可能なレイアウト
  • Menuと比べて柔軟なコンテンツ配置ができる

❌ Bad

  • 重要な操作や確認に使う
  • 大量のコンテンツを詰め込む
  • スクロールが必要なほどの長いコンテンツ

✅ Good

  • 重要な操作はModal/Dialogを使用する
  • コンテンツは必要最小限にする
  • 長いコンテンツはDrawerやModalを使用する

実装例

import { useState } from 'react'
import {
  Popover,
  Button,
  Typography,
  Box,
  IconButton,
  List,
  ListItem,
  ListItemText,
  Divider,
} from '@mui/material'
import { Close as CloseIcon } from '@mui/icons-material'

export const SimplePopover = () => {
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)

  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    setAnchorEl(event.currentTarget)
  }

  const handleClose = () => {
    setAnchorEl(null)
  }

  const open = Boolean(anchorEl)
  const id = open ? 'simple-popover' : undefined

  return (
    <>
      <Button variant="contained" onClick={handleClick}>
        詳細を表示
      </Button>
      <Popover
        id={id}
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'left',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'left',
        }}
      >
        <Box sx={{ width: 300, p: 2 }}>
          <Box
            sx={{
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              mb: 1,
            }}
          >
            <Typography variant="h6">詳細情報</Typography>
            <IconButton size="small" onClick={handleClose}>
              <CloseIcon />
            </IconButton>
          </Box>
          <Divider sx={{ mb: 2 }} />
          <List dense>
            <ListItem>
              <ListItemText primary="作成日" secondary="YYYY/MM/DD" />
            </ListItem>
            <ListItem>
              <ListItemText primary="ステータス" secondary="処理中" />
            </ListItem>
          </List>
        </Box>
      </Popover>
    </>
  )
}

6. Drawer

  • 画面端からスライドイン
  • 大量のコンテンツを表示できる
  • サイドバーやナビゲーションに最適
  • 画面の一部を占有しながら、メインコンテンツも表示可能

❌ Bad

  • 重要な確認ダイアログをDrawerで実装
  • 画面全体を覆うコンテンツ
  • 短いメニュー

✅ Good

  • 確認にはModal/Dialogを使用
  • コンテンツ幅を適切に指定する
  • 設定項目やフォームを表示
  • 複数の項目を持つサイドバー

実装例

import { useState } from 'react'
import {
  Drawer,
  List,
  ListItem,
  ListItemButton,
  ListItemIcon,
  ListItemText,
  IconButton,
  Box,
  Typography,
  Divider,
} from '@mui/material'
import {
  Menu as MenuIcon,
  Home as HomeIcon,
  Person as PersonIcon,
  Settings as SettingsIcon,
  Mail as MailIcon,
  Help as HelpIcon,
} from '@mui/icons-material'

export const SimpleDrawer = () => {
  const [open, setOpen] = useState(false)

  const menuItems = [
    { text: 'ホーム', icon: <HomeIcon />, path: '/' },
    { text: 'プロフィール', icon: <PersonIcon />, path: '/profile' },
    { text: 'メッセージ', icon: <MailIcon />, path: '/messages' },
    { text: '設定', icon: <SettingsIcon />, path: '/settings' },
    { text: 'ヘルプ', icon: <HelpIcon />, path: '/help' },
  ]

  return (
    <>
      <IconButton edge="start" color="inherit" onClick={() => setOpen(true)}>
        <MenuIcon />
      </IconButton>

      <Drawer anchor="left" open={open} onClose={() => setOpen(false)}>
        <Box sx={{ width: 280 }}>
          <Box sx={{ p: 2 }}>
            <Typography variant="h6">サイドメニュー</Typography>
          </Box>
          <Divider />
          <List>
            {menuItems.map((item) => (
              <ListItem key={item.text} disablePadding>
                <ListItemButton onClick={() => setOpen(false)}>
                  <ListItemIcon>{item.icon}</ListItemIcon>
                  <ListItemText primary={item.text} />
                </ListItemButton>
              </ListItem>
            ))}
          </List>
        </Box>
      </Drawer>
    </>
  )
}

7. Tooltip

  • ホバーやフォーカスで一時的に表示
  • シンプルなヘルプテキスト
  • 簡潔な補足情報の表示(アイコンボタンの説明等)

❌ Bad

  • 長文を入れる
  • 重要な情報の表示
  • インタラクティブな要素を入れる

✅ Good

  • 長文にはPopperを使用する
  • インタラクティブな要素にはMenuやModalを使用する

実装例

import { Box, IconButton, Tooltip } from '@mui/material'
import {
  Edit as EditIcon,
  Delete as DeleteIcon,
  Share as ShareIcon,
} from '@mui/icons-material'

export const SimpleTooltip = () => {
  return (
    <Box sx={{ display: 'flex', gap: 1 }}>
      <Tooltip title="編集する" arrow placement="top">
        <IconButton>
          <EditIcon />
        </IconButton>
      </Tooltip>

      <Tooltip title="共有する" arrow placement="top">
        <IconButton>
          <ShareIcon />
        </IconButton>
      </Tooltip>

      <Tooltip title="削除する" arrow placement="top">
        <IconButton color="error">
          <DeleteIcon />
        </IconButton>
      </Tooltip>
    </Box>
  )
}

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?