モーダルとダイアログ
ルール
- Radix Dialog または shadcn
<Dialog> を使用 — アクセシブルで、フォーカストラップ、スクロールロックがデフォルトで有効
- 破壊的操作には確認ダイアログを使用:"このプロジェクトを削除しますか?" に対して、キャンセル(セカンダリ)と削除(破壊的)ボタンを配置
- モバイルではシート/ドロワーを使用:shadcn の
<Sheet> を利用 — 下または横からスライドし、中央配置のモーダルよりタッチターゲットが良好
- フォーカストラップ:Radix が自動処理 — 開くと最初のフォーカス可能要素にフォーカスが移動し、閉じると元の位置に戻る
- スクロールロック:モーダルが開いているときに body のスクロールを防止 — Radix が
<Dialog.Overlay> で処理
- 閉じるトリガー:Escape キー、オーバーレイ外のクリック、明示的な閉じるボタン — すべて Radix が提供
- 制御状態:
open + onOpenChange プロパティを使用 — 外部イベントによってトリガーされるモーダルでは内部状態に依存しない
- ダイアログ内のフォーム:Enter で送信、成功時に閉じる、エラーはインライン表示 — バリデーション失敗時に閉じない
- モバイル:小さな画面でのアクションには中央配置の
<Dialog> ではなく、<Sheet>(下部ドロワー)を使用
パターン
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,
AlertDialogTitle, AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
function DeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
const [loading, setLoading] = useState(false);
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">プロジェクトを削除</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>このプロジェクトを削除しますか?</AlertDialogTitle>
<AlertDialogDescription>
この操作はプロジェクトとそのすべてのデータを完全に削除します。この操作は元に戻せません。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>キャンセル</AlertDialogCancel>
<AlertDialogAction
onClick={async () => { setLoading(true); await onDelete(); setLoading(false); }}
disabled={loading}
>
{loading ? "削除中..." : "削除"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
function MobileMenu() {
return (
<Sheet>
<SheetTrigger asChild><Button variant="ghost" size="icon"><MenuIcon /></Button></SheetTrigger>
<SheetContent side="bottom">
<SheetHeader><SheetTitle>アクション</SheetTitle></SheetHeader>
<nav className="flex flex-col gap-2 py-4">
<Button variant="ghost">編集</Button>
<Button variant="ghost">共有</Button>
<Button variant="destructive">削除</Button>
</nav>
</SheetContent>
</Sheet>
);
}
避けるべきこと
- カスタムモーダルを
position: fixed と手動フォーカス管理で作成しない — Radix/shadcn Dialog を使用すること
- Escape キーとオーバーレイ外クリックでの閉じる動作を省略しない — Radix が両方を処理するため、上書きしないこと
- モバイルのアクションメニューに中央配置のモーダルを使用しない — 親指のリーチを考慮して下部 Sheet/ドロワーを使用すること
- フォームのバリデーション失敗時にダイアログを閉じない — 開いたままにし、インラインエラーを表示してユーザーが修正・再送信できるようにすること
- 適切なスタッキングコンテキストなしでダイアログをネストしない — 必要な場合は、Radix のネストされた Dialog サポートを適切な
z-index とともに使用すること