概要
Radix UI の Dialog は WAI-ARIA 準拠のモーダルを提供する headless コンポーネント。 フォーカストラップ・スクリーンリーダー対応・Escape キーでの閉鎖などが組み込まれている。
インストール
npm install @radix-ui/react-dialog
基本実装
import * as Dialog from "@radix-ui/react-dialog";
export function ConfirmDialog() {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700">
削除する
</button>
</Dialog.Trigger>
<Dialog.Portal>
{/* オーバーレイ */}
<Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
{/* コンテンツ */}
<Dialog.Content className="fixed left-1/2 top-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg focus:outline-none">
<Dialog.Title className="text-lg font-semibold text-gray-900">
本当に削除しますか?
</Dialog.Title>
<Dialog.Description className="mt-2 text-sm text-gray-500">
この操作は元に戻せません。
</Dialog.Description>
<div className="mt-6 flex justify-end gap-3">
<Dialog.Close asChild>
<button className="rounded border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
キャンセル
</button>
</Dialog.Close>
<button
onClick={() => console.log("deleted")}
className="rounded bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700"
>
削除
</button>
</div>
<Dialog.Close className="absolute right-4 top-4 text-gray-400 hover:text-gray-600">
✕
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
制御コンポーネント(open を外部管理)
import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
export function ControlledDialog() {
const [open, setOpen] = useState(false);
const handleConfirm = () => {
// 処理実行
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button onClick={() => setOpen(true)}>開く</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg">
<Dialog.Title>確認</Dialog.Title>
<div className="mt-4 flex gap-3">
<button onClick={() => setOpen(false)}>キャンセル</button>
<button onClick={handleConfirm}>確認</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
ポイント
Dialog.Portalでモーダルをbody直下にレンダリングする(z-index 問題を回避)asChildを使うと任意の要素をトリガーにできるdata-[state=open]/data-[state=closed]でアニメーションを制御できる- フォーカストラップは自動的に適用される