web-ui-radix-ui について
Radix UI のスタイルを持たない React プリミティブを扱うガイドです。独自の UI を構築する開発者向けに、複合コンポーネント、アクセシビリティ動作、要素の合成、アニメーション用の状態フックを説明します。
主な機能
- 01スタイルなしのコンポーネントプリミティブ
- 02共有コンテキストを使う複合コンポーネント
- 03ARIA とキーボード操作のサポート
- 04asChild による要素合成
- 05data-state によるアニメーションフック
活用シーン
- 01ダイアログとオーバーレイの構築
- 02メニュー、セレクト、ツールチップの実装
- 03カスタムボタンとトリガー要素の合成
- 04状態に応じたコンポーネントアニメーション
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-ui-radix-uiインストール後、Claude Code で/web-ui-radix-uiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/animation.mdcore.mdforms.mdmenus.mdnavigation.mdoverlays.mdpreview.md詳細
Radix UI Primitives
クイックガイド: Radix が提供するのは、コンポーネントの動作とアクセシビリティを備え、見た目を持たないプリミティブです。すべてのプリミティブは複合コンポーネントであり、
Root、Trigger、Portal、Contentがコンテキストを共有します。asChildを使うと、プリミティブの動作を独自の要素にマージできます。data-state属性によって CSS アニメーションを制御します。現在のリリースは v1.8.x です。React 19 と RSC に対応し、forwardRefは不要になりました。また、radix-uiは個別の@radix-ui/react-*パッケージに代わる統合パッケージです。
詳細なリソース:
- examples/core.md — Dialog、
asChild、Slot.Root、Slot.Slottable、Slot.createSlot - examples/overlays.md — AlertDialog、非同期処理を伴う制御された Dialog
- examples/menus.md — DropdownMenu、サブメニュー、チェックボックス項目とラジオ項目
- examples/forms.md — Select、CSP の
nonce、不確定状態の Progress - examples/navigation.md — Accordion と Tabs
- examples/animation.md —
data-stateのキーフレームと高さのアニメーション - examples/preview.md —
unstable_プリミティブ:OTP フィールド、パスワード切り替え、Form - reference.md — コード付きのアンチパターン、チェックリスト、CSS 変数の一覧、v1.8.x までの主な変更点
該当するパス
- 安定版プリミティブ — 8 年にわたる API サーフェスで、セマンティックバージョニングに基づいて安定しています。examples/core.md に続けて、必要なプリミティブファミリーのファイルを参照してください。
- プレビュー版プリミティブ —
unstable_OneTimePasswordField、unstable_PasswordToggleField、unstable_Form。プレフィックスが API の契約です。マイナーバージョン間で変更されるため、examples/preview.md を参照し、アップグレード時に見直すことを想定してください。 - 表示・非表示のアニメーション — 複雑さに応じて方法が分かれます。
data-stateに連動した CSS@keyframesでフェード、スライド、高さのアニメーションに対応できます。シーケンスを制御するにはforceMountが必要です。どちらも examples/animation.md で説明しています。
<critical_requirements>
Radix UI のコードを書く前に
個別の @radix-ui/react-* パッケージではなく、統合された radix-ui パッケージをインストールしてください。 共通の内部実装を同じバージョンにすることで、コンテキストプロバイダーの重複やプリミティブ間の重なり順の競合を防げます。
オーバーレイには完全な構成要素を用意してください — Root、Trigger、Portal、Overlay、Content、Close。 各パーツは Root のコンテキストに登録されます。パーツが欠けると、単にマークアップがなくなるだけでなく、そのパーツが担っていた動作も失われます。
オーバーレイのコンテンツは Portal で囲んでください。 その場でレンダリングされたコンテンツは、すべての祖先要素の overflow: hidden とスタッキングコンテキストを継承するため、スクロールパネル内のダイアログは切り取られてしまいます。
すべての Dialog と AlertDialog に Title を用意してください。 これはスクリーンリーダーが開いたときに読み上げるものであり、v1.6.0 以降、Radix は Title がない場合でもエラーを記録しなくなりました。
asChild と組み合わせて使うコンポーネントでは、ref を転送し、すべての props を展開してください。 Radix がフローティングコンテンツを配置し、閉じたときにフォーカスを戻すには ref が必要です。また、ARIA 属性とイベントハンドラーを DOM 要素に渡すには props の展開が必要です。React 19 では ref は通常の prop として渡されます。それ以前のバージョンでは forwardRef でラップする必要があります。
</critical_requirements>
自動検出: radix-ui, @radix-ui, Dialog.Root, AlertDialog, DropdownMenu, Popover, Tooltip, Accordion, Tabs, Select.Viewport, asChild, Slot, Slottable, ItemIndicator, data-state, forceMount, onOpenAutoFocus, onInteractOutside, --radix-accordion-content-height, unstable_OneTimePasswordField, unstable_PasswordToggleField, unstable_Form
対象範囲:
- オーバーレイの動作 — フォーカストラップ、閉じる操作、衝突を考慮した位置決め、重なり順
- 共有コンテキストを通じて各パーツが連携する複合コンポーネント API
- メニュー、セレクト、タブ、アコーディオンのキーボード操作と ARIA の設定
- ポリモーフィズム —
asChildを使って、プリミティブの動作を別の要素でレンダリングすること Slotを使って、asChildに対応した独自コンポーネントを作成すること
別途扱う内容:
- ビジュアルデザイン — プリミティブはスタイルなしでレンダリングされ、
classNameを受け取ります。そのクラスを何で生成するかは、このスキルの対象外です - フォームの状態、スキーマ検証、送信 — Radix の
SelectやCheckboxが通知するのは値のみで、それ以上の機能はありません - アイコンの図柄 — 例では
aria-hiddenを指定した Unicode グリフを使用しています。実際のアイコンを用意するかどうかは別途選択します - JavaScript によるアニメーションのオーケストレーション — Radix は、表示状態を検出するアニメーションランタイムが利用するフックとして
forceMountとdata-stateを公開しています
Radix は、通常なら一体化しているコンポーネントの二つの側面を分離します。ARIA ロール、フォーカス順序、閉じる操作のセマンティクス、衝突検出といった、難しく目に見えない側面は Radix が担い、プロダクトのアイデンティティとなる側面には一切手を加えません。
この分離には、しっかり理解しておくべき重要な帰結があります。プリミティブは、props で設定するコンポーネントではなく、パーツの集合です。 Dialog は設定する一つの要素ではありません。Root、Trigger、Portal、Overlay、Content、Title、Description、Close が、目に見えないコンテキストを介して連携します。必要なパーツをデザインに合った構成で組み合わせれば、連携は自動的に行われます。パーツが用意されているのに prop を使おうとするのは、たいていこのモデルに逆らっている兆候です。
<decision_framework>
プリミティブの選び方
Blocking the page?
├─ Destructive confirmation → AlertDialog (no click-outside dismiss, by design)
├─ Form or content → Dialog
└─ Page-level, not blocking → Dialog with modal={false}, or Popover
Floating beside a trigger?
├─ Opened by click, holds interactive content → Popover
├─ Opened by hover or focus, text only → Tooltip
└─ A list of actions → DropdownMenu
Choosing from options?
├─ One value, in a form → Select
├─ Several values → Popover containing Checkbox items
└─ An action, not a value → DropdownMenu
Showing and hiding regions?
├─ One at a time, in place → Accordion (type="single" collapsible)
├─ Several at once → Accordion (type="multiple")
└─ One panel replacing another → Tabs
</decision_framework>
基本パターン
パターン 1:複合コンポーネントの構成
すべてのオーバーレイは同じ六つのパーツで構成されます。Root が状態を保持し、Portal がレイアウトの制約から抜け出し、Title と Description がアクセシブルな名前を提供します。
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className={overlayClass} />
<Dialog.Content className={contentClass}>
<Dialog.Title>Account settings</Dialog.Title>
<Dialog.Description>Manage preferences and security.</Dialog.Description>
<Dialog.Close aria-label="Close">×</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
コード全体:examples/core.md
### パターン 2:制御状態と非制御状態defaultOpen を指定すると、Radix が状態を管理します。open と onOpenChange を指定すると、その管理を引き継げます。非同期保存の後に閉じる場合、トリガー以外の場所から開く場合、または URL と状態を同期する場合に必要です。
onOpenChange はユーザーの意図を伝える唯一の手段なので、これを省略した制御プリミティブは閉じることができません。
const [open, setOpen] = useState(false);
<Dialog.Root open={open} onOpenChange={setOpen}>
{/* … */}
</Dialog.Root>;
コード全体:examples/overlays.md
パターン 3:多態性のための asChild
asChild は、パーツ自身の要素ではなく子要素をレンダリングし、プロパティと動作をその要素にマージするよう指示します。これにより、ラッパー要素を使わずに Tooltip.Trigger をアンカーにしたり、Dialog.Trigger を独自のボタンにしたりできます。
<Tooltip.Trigger asChild>
<a href="/docs">Documentation</a>
</Tooltip.Trigger>
この位置に置くカスタムコンポーネントは、ref を受け取り、残りのプロパティを展開する必要があります。そうしないと、Radix から渡されたイベントハンドラーや ARIA 属性がマージ時に黙って失われます。
コード全体:examples/core.md
パターン 4:Slot を使った独自の asChild コンポーネントの構築
Slot は、asChild が使用するマージ機能を独自のコンポーネント向けに公開したものです。asChild が設定されている場合は、レンダリングする要素を Slot.Root に置き換えます。コンポーネントが固定の装飾もレンダリングする場合、Slot.Slottable で子のコンテンツを配置する場所を指定します。
function Button({ asChild = false, ...props }: ButtonProps) {
const Comp = asChild ? Slot.Root : "button";
return <Comp {...props} />;
}
child={children} と、子としてレンダリング関数を指定した Slot.Slottable は、子自体にマージ済みのプロパティを保持したまま、そのコンテンツを独自のマークアップで囲みます。
Slot.createSlot("Button") は、エラーメッセージに Button と表示する Slot を返し、レンダリングする要素とそのプロパティの型引数を受け取ります。
コード全体:examples/core.md
パターン 5:Portal とそのコンテナー
Portal は、コンテンツを document.body に移動し、すべての祖先要素によるクリッピングやスタッキングコンテキストの外に配置します。container を渡すと、シャドウルート、iframe の body、マイクロフロントエンド独自のマウントポイントなど、別の場所に配置できます。
<Popover.Portal container={containerRef.current}>
<Popover.Content>
<Popover.Arrow />
</Popover.Content>
</Popover.Portal>
コード全体:examples/core.md
パターン 6:data-state によるアニメーション
Radix は、アニメーション可能なパーツに data-state="open" と "closed" を設定し、終了アニメーションが完了したと通知されるまでアンマウントを一時停止します。animationend を監視するため、@keyframes はアンマウントを遅らせますが、transition は遅らせません。
.dialog-overlay[data-state="open"] {
animation: fadeIn 150ms ease-out;
}
.dialog-overlay[data-state="closed"] {
animation: fadeOut 150ms ease-in;
}
JavaScript ランタイムで制御するシーケンスの場合、Portal、Overlay、Content に forceMount を指定すると、アンマウントの制御を自分で行えます。
コード全体:examples/animation.md
パターン 7:フォーカス管理
モーダルコンテンツは設定不要でフォーカスを閉じ込め、閉じるとトリガーにフォーカスを戻します。onOpenAutoFocus で初期フォーカスの位置を変更できます。破壊的な操作の確認画面では、Delete ではなく Cancel にフォーカスを移して開くべきです。
<AlertDialog.Content
onOpenAutoFocus={(event) => {
event.preventDefault();
cancelRef.current?.focus();
}}
>
コード全体:examples/overlays.md
パターン 8:オーバーレイのアクセシブルな名前
Title はアクセシブルな名前であり、必須です。デザイン上、見出しを表示しない場合でも、要素は残して視覚的に隠してください。要素を削除すると、テキストだけでなく読み上げもなくなります。
<VisuallyHidden asChild>
<Dialog.Title>Filter results</Dialog.Title>
</VisuallyHidden>
Description を省略する場合、回避策は必要ありません。v1.6.3 以降、Radix はレンダリングされた場合にのみ aria-describedby を設定します。
コード全体:examples/core.md
<red_flags>
注意すべき点
実行時に問題が起きます:
refを受け取らないカスタムのasChild子要素 — Radix はサイズを測定したり、フォーカスを設定したりできません。そのため、フローティングコンテンツの位置がずれ、フォーカスも戻りません。refをプロパティとして受け取るようにする(React 19)、またはforwardRefでラップしてください- 残りのプロパティを展開しないカスタムの
asChild子要素 — マージされたイベントハンドラーと ARIA 属性が破棄され、見た目は正しいのに何も動作しないトリガーになります Portalの外にあるオーバーレイコンテンツ —overflow: hiddenを持つ祖先要素によって切り取られ、誤ったスタッキングコンテキストに重ねられますTitleのない Dialog — アクセシブルな名前がなく、v1.6.0 以降はそれを知らせるコンソールエラーも表示されません- 終了アニメーションに
transitionを使用する — アンマウントは遅延されないため、アニメーションが始まる前に要素が消えます。@keyframesを使用してください forceMountを使わずに JavaScript アニメーションランタイムでコンテンツをマウントする — 状態が切り替わった瞬間に Radix がアンマウントするため、終了アニメーションは最初のフレームで中断されます
意外な動作:
- 終了アニメーションが始まった後ではなく、始まる前に
data-stateが"closed"に切り替わります --radix-accordion-content-heightとその関連変数は、アニメーションの実行中のみ設定され、アニメーション外では空として読み取られます- AlertDialog にはクリックアウトや Escape による閉じる操作がありません。終了するには
CancelまたはActionを使うしかありません。それが AlertDialog を選ぶ目的です Slotはハンドラーを置き換えるのではなくマージし、子要素自身のハンドラーが先に実行されます- ポータル化されたコンテンツに明示的な
z-indexを設定すると、レイヤー化されたプリミティブ間で Radix が維持する順序と競合します。代わりにコンテナーを分離してください value={undefined}を指定したProgressは、0 ではなく不確定状態になりますSelectとScrollAreaはnonceプロパティを受け取ります。これは、nonce のないインラインスタイルがブロックされるコンテンツセキュリティポリシーの環境で必要です