radix-ui について
ReactのWeb開発向けの、スタイルを持たないアクセシブルなUIプリミティブです。キーボード操作、フォーカス管理、ARIA属性を備え、DialogやSelectなどの構成・スタイル設定・アクセシビリティの実践方法を扱います。
主な機能
- 01スタイルを持たないReact UIプリミティブ
- 02キーボード操作とフォーカス管理を内蔵
- 03ARIA属性を自動付与
- 04Dialog、Select、Tooltipを解説
- 05Tailwindのスタイル設定とテスト方法
活用シーン
- 01Webアプリのダイアログ構築
- 02キーボード操作可能なドロップダウンの実装
- 03インタラクティブなUIのスクリーンリーダー対応確認
MCP 対応
READMEでは、完全なAPIリファレンス、コンポーネントのプロパティ、例を調べるため、Context7 MCPの`resolve-library-id`と`query-docs`を使うよう案内しています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dralgorhythm/ claude-agentic-framework --skill radix-uiインストール後、Claude Code で/radix-uiを入力して読み込まれたか確認します。
詳細
Radix UI
プラットフォーム: Web のみです。モバイルのモーダル/シートについては、expo-sdk と react-native-patterns のスキルを参照してください。
API リファレンス、すべてのコンポーネントのプロパティ、追加の例については、Context7 MCP(
resolve-library-idの後にquery-docs)を使用してください。
概要
キーボードナビゲーション、フォーカス管理、ARIA 属性を標準で備えた、スタイル未設定のアクセシブルな React 用 UI プリミティブです。Tailwind CSS と Framer Motion と組み合わせて使うよう設計されています。
バージョン:最新(個別パッケージ)または radix-ui 統合パッケージ
インストール(個別パッケージ):
pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-select @radix-ui/react-tooltip @radix-ui/react-tabs
インストール(統合パッケージ):
pnpm add radix-ui
統合パッケージの radix-ui にはすべてのプリミティブが含まれています。依存関係の管理を簡素化したい場合はこちらを使用してください。
ワークフロー
Dialog の追加:
- インストール:
pnpm add @radix-ui/react-dialog - Dialog のパーツをインポート:Root、Trigger、Portal、Overlay、Content
- 適切にスタックされるよう、Overlay と Content を Portal で囲む
- Tailwind と data-[state=] セレクターでスタイルを設定する
- キーボードナビゲーションをテストする(Esc キーで閉じる、Tab キーをモーダル内にとどめる)
- 必要に応じて Framer Motion のアニメーションを追加する
Select の追加:
- インストール:
pnpm add @radix-ui/react-select - Select のパーツをインポート:Root、Trigger、Portal、Content、Item
- 視覚的なフィードバックのため、Trigger に Icon と Value を追加する
- data-[state=open] で開閉状態のスタイルを設定する
- キーボード操作をテストする(矢印キー、Enter、タイプアヘッド)
- Portal の z-index が適切であることを確認する
ツールチップの追加:
- インストール:
pnpm add @radix-ui/react-tooltip - アプリを TooltipProvider で一度だけルートで囲む
- 各ツールチップで Trigger と Content を組み合わせる
- ホバー時の表示タイミングを設定するため、delayDuration を指定する
- スクリーンリーダーで読み上げられることを確認する
Dialog(モーダル)
import * as Dialog from '@radix-ui/react-dialog';
// OR: import { Dialog } from 'radix-ui';
function ModalExample() {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button className="px-4 py-2 bg-blue-600 text-white rounded">Open Dialog</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-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] max-h-[85vh] w-[90vw] max-w-[500px] rounded-lg bg-white p-6 shadow-lg">
{/* Title and Description are required for accessibility */}
<Dialog.Title className="text-lg font-semibold mb-2">Dialog Title</Dialog.Title>
<Dialog.Description className="text-sm text-gray-600 mb-4">
Screen readers announce this description.
</Dialog.Description>
<div className="flex justify-end gap-2 mt-6">
<Dialog.Close asChild>
<button className="px-4 py-2 border rounded">Cancel</button>
</Dialog.Close>
<button className="px-4 py-2 bg-blue-600 text-white rounded">Save</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Framer Motion のアニメーション:Portal に forceMount を指定し、AnimatePresence で囲みます。Overlay/Content に asChild を渡し、子要素として motion.div を使用します。
Select(ドロップダウン)
import * as Select from '@radix-ui/react-select';
import { ChevronDownIcon, CheckIcon } from '@radix-ui/react-icons';
function SelectExample() {
return (
<Select.Root defaultValue="apple">
<Select.Trigger className="inline-flex items-center justify-between rounded px-4 py-2 text-sm bg-white border gap-2 data-[placeholder]:text-gray-400 min-w-[200px]">
<Select.Value placeholder="Select a fruit..." />
<Select.Icon><ChevronDownIcon /></Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="overflow-hidden bg-white rounded-md shadow-lg border">
<Select.Viewport className="p-1">
<Select.Item value="apple" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 outline-none cursor-pointer data-[disabled]:opacity-50">
<Select.ItemIndicator className="absolute left-2"><CheckIcon /></Select.ItemIndicator>
<Select.ItemText>Apple</Select.ItemText>
</Select.Item>
<Select.Item value="banana" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 outline-none cursor-pointer">
<Select.ItemIndicator className="absolute left-2"><CheckIcon /></Select.ItemIndicator>
<Select.ItemText>Banana</Select.ItemText>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}
グループ化した選択肢には Select.Group と Select.Label を、区切り線には Select.Separator を使用します。
Tailwind によるスタイリング
主なデータ属性セレクター
// Open/closed state
className="data-[state=open]:bg-blue-50 data-[state=closed]:bg-gray-50"
// Disabled
className="data-[disabled]:opacity-50 data-[disabled]:pointer-events-none"
// Checked (checkboxes, radio items)
className="data-[state=checked]:bg-blue-600"
// Side-based positioning
className="data-[side=top]:animate-slide-down data-[side=bottom]:animate-slide-up"
よく使われるレイアウトパターン
// Focus ring (required for keyboard UX)
className="outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
// Centered modal
className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]"
// Backdrop overlay
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
アクセシビリティ
キーボードナビゲーション(自動処理):
- Dialog:Esc キーで閉じる。Tab キーはモーダル内にとどまる
- Select:矢印キーで移動し、Enter キーで選択。タイプアヘッド検索
- DropdownMenu:矢印キーで移動し、Enter キーで選択
- Tabs:矢印キーで移動し、Home/End キーで先頭/末尾へ移動
- Tooltip:トリガーにフォーカスするとツールチップを表示
ARIA(自動付与):
- Dialog:
role="dialog"、aria-labelledby(Title)、aria-describedby(Description) - Select:
role="combobox"、aria-expanded、aria-controls
スクリーンリーダーでの読み上げに必要なため、必ず Dialog.Title と Dialog.Description を指定してください。
カスタムのフォーカス管理:
<Dialog.Content
onOpenAutoFocus={(e) => { e.preventDefault(); customRef.current?.focus(); }}
onCloseAutoFocus={(e) => { e.preventDefault(); triggerRef.current?.focus(); }}
>
制御ありと制御なし
// Uncontrolled — component manages its own state
<Dialog.Root defaultOpen={false}>
// Controlled — parent manages state (preferred for complex UIs)
const [open, setOpen] = useState(false);
<Dialog.Root open={open} onOpenChange={setOpen}>
Portal の使用
z-index の競合、overflow: hidden によるクリッピング、CSS transform の問題を避けるため、オーバーレイやドロップダウンは必ず Portal に描画してください。
<Dialog.Portal> {/* renders at document.body */}
<Dialog.Overlay />
<Dialog.Content />
</Dialog.Portal>
{/* Custom container */}
<Dialog.Portal container={customContainerRef.current}>
ベストプラクティス
asChildプロパティを使用すると、ラッパーの div なしでカスタム要素を組み合わせられます- オーバーレイやドロップダウンは必ず Portal を使用すると、z-index の問題を避けられます
- すべての Dialog に Title と Description を指定する(アクセシビリティ要件)
- 開閉状態のスタイリングには data-[state=] セレクターを使用する
- 複雑な状態管理には制御ありのパターンを優先する
- TooltipProvider はアプリのルートに 1 つだけ配置する。ツールチップごとには配置しないでください
- インタラクティブなコンポーネントはすべてキーボードナビゲーションをテストする
アンチパターン
- ❌
Dialog.Portalを忘れる(z-index の問題が発生します) - ❌
Dialog.TitleまたはDialog.Descriptionがない(アクセシビリティ要件を満たせません) - ❌ カスタムトリガーに
asChildを使わない(ラッパーの div が作成されます) - ❌
TooltipProviderを複数作成する(不要なオーバーヘッドが発生します) - ❌
onOpenChange/onValueChangeを指定せずに制御ありのパターンを使う - ❌ 制御ありと制御なしのパターンを混在させる
- ❌
focus:ringのスタイルを忘れる(キーボード操作の使い勝手が低下します) - ❌ キーボードナビゲーションでテストしない
フィードバックループ
アクセシビリティテスト:
# Test with keyboard only (no mouse)
# Tab through all interactive elements
# Esc should close Dialogs, Dropdowns, Selects
# Arrow keys should navigate menus and selects
スクリーンリーダーのテスト:
# macOS: VoiceOver (Cmd+F5)
# Verify Dialog.Title and Dialog.Description are announced
# Check for proper role attributes
ビジュアルリグレッション: すべての状態をテストします。閉じた状態と開いた状態、ホバー時とフォーカス時、選択状態と未選択状態、無効状態、さまざまなビューポートサイズを確認します。
Framer Motion: forceMount と AnimatePresence を使用します。アニメーション中もフォーカス管理が機能することをテストします。