レビュー済み·難易度: 中級·更新: 2026-04-15

Radix UI の Dialog でアクセシブルなモーダルを作る

Radix UI の Dialog コンポーネントを使ってアクセシビリティに配慮したモーダルダイアログを実装する。Tailwind CSS でスタイリングする例も示す。

reactui-componentradix-uitailwindcss

対応バージョン

react 19typescript 5radix-ui 1tailwindcss 4

前提環境

React の基本的な使い方(useState, イベントハンドラ)を理解していること

概要

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] でアニメーションを制御できる
  • フォーカストラップは自動的に適用される

注意点

Radix UI はスタイルを持たない headless UI ライブラリ。スタイリングは Tailwind CSS などで行う

関連サンプル