Radix UI primitive components for accessible UI. Use when building accessible interactive components.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers Radix UI primitives for building accessible, unstyled components.
Use this skill when:
ACCESSIBILITY FIRST - Radix handles focus management, keyboard navigation, and ARIA attributes. You handle styling.
# Install individual primitives
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-popover
npm install @radix-ui/react-tooltip
npm install @radix-ui/react-tabs
npm install @radix-ui/react-accordion
npm install @radix-ui/react-select
npm install @radix-ui/react-checkbox
npm install @radix-ui/react-switch
npm install @radix-ui/react-slider
'use client';
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
children: React.ReactNode;
}
export function Modal({
open,
onOpenChange,
title,
description,
children,
}: ModalProps): React.ReactElement {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<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%] bg-white rounded-lg p-6 w-full max-w-md shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
<Dialog.Title className="text-lg font-semibold">
{title}
</Dialog.Title>
{description && (
<Dialog.Description className="mt-2 text-sm text-gray-500">
{description}
</Dialog.Description>
)}
<div className="mt-4">{children}</div>
<Dialog.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// Usage
function App(): React.ReactElement {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal
open={open}
onOpenChange={setOpen}
title="Confirm Action"
description="Are you sure you want to proceed?"
>
<div className="flex gap-2 justify-end">
<button onClick={() => setOpen(false)}>Cancel</button>
<button onClick={() => { /* action */ setOpen(false); }}>
Confirm
</button>
</div>
</Modal>
</>
);
}
'use client';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight } from 'lucide-react';
interface MenuItem {
label: string;
onSelect: () => void;
disabled?: boolean;
icon?: React.ReactNode;
}
interface MenuProps {
trigger: React.ReactNode;
items: MenuItem[];
}
export function Menu({ trigger, items }: MenuProps): React.ReactElement {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
{trigger}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[200px] bg-white rounded-md shadow-lg border p-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2"
sideOffset={5}
>
{items.map((item, index) => (
<DropdownMenu.Item
key={index}
disabled={item.disabled}
onSelect={item.onSelect}
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100 data-[disabled]:opacity-50 data-[disabled]:pointer-events-none"
>
{item.icon}
{item.label}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
// With sub-menus and checkboxes
function AdvancedMenu(): React.ReactElement {
const [showBookmarks, setShowBookmarks] = useState(true);
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button>Options</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="menu-content">
<DropdownMenu.CheckboxItem
checked={showBookmarks}
onCheckedChange={setShowBookmarks}
className="menu-item"
>
<DropdownMenu.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenu.ItemIndicator>
Show Bookmarks
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger className="menu-item">
More Options
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent className="menu-content">
<DropdownMenu.Item className="menu-item">
Option 1
</DropdownMenu.Item>
<DropdownMenu.Item className="menu-item">
Option 2
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
'use client';
import * as Tabs from '@radix-ui/react-tabs';
interface TabItem {
value: string;
label: string;
content: React.ReactNode;
}
interface TabsGroupProps {
defaultValue: string;
items: TabItem[];
}
export function TabsGroup({ defaultValue, items }: TabsGroupProps): React.ReactElement {
return (
<Tabs.Root defaultValue={defaultValue}>
<Tabs.List className="flex border-b">
{items.map((item) => (
<Tabs.Trigger
key={item.value}
value={item.value}
className="px-4 py-2 text-sm border-b-2 border-transparent data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 hover:text-gray-700"
>
{item.label}
</Tabs.Trigger>
))}
</Tabs.List>
{items.map((item) => (
<Tabs.Content
key={item.value}
value={item.value}
className="p-4 focus:outline-none"
>
{item.content}
</Tabs.Content>
))}
</Tabs.Root>
);
}
'use client';
import * as Select from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
interface SelectOption {
value: string;
label: string;
}
interface SelectFieldProps {
value: string;
onValueChange: (value: string) => void;
options: SelectOption[];
placeholder?: string;
}
export function SelectField({
value,
onValueChange,
options,
placeholder = 'Select...',
}: SelectFieldProps): React.ReactElement {
return (
<Select.Root value={value} onValueChange={onValueChange}>
<Select.Trigger className="inline-flex items-center justify-between gap-2 px-3 py-2 text-sm bg-white border rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500">
<Select.Value placeholder={placeholder} />
<Select.Icon>
<ChevronDown className="h-4 w-4" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="bg-white rounded-md shadow-lg border overflow-hidden">
<Select.ScrollUpButton className="flex items-center justify-center h-6">
<ChevronUp className="h-4 w-4" />
</Select.ScrollUpButton>
<Select.Viewport className="p-1">
{options.map((option) => (
<Select.Item
key={option.value}
value={option.value}
className="relative flex items-center px-8 py-2 text-sm rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100 data-[state=checked]:text-blue-600"
>
<Select.ItemIndicator className="absolute left-2">
<Check className="h-4 w-4" />
</Select.ItemIndicator>
<Select.ItemText>{option.label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
<Select.ScrollDownButton className="flex items-center justify-center h-6">
<ChevronDown className="h-4 w-4" />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}
'use client';
import * as Tooltip from '@radix-ui/react-tooltip';
interface TooltipProps {
content: string;
children: React.ReactNode;
side?: 'top' | 'right' | 'bottom' | 'left';
}
export function TooltipWrapper({
content,
children,
side = 'top',
}: TooltipProps): React.ReactElement {
return (
<Tooltip.Provider delayDuration={300}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
{children}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side={side}
className="px-3 py-1.5 text-sm bg-gray-900 text-white rounded shadow-lg animate-in fade-in-0 zoom-in-95"
sideOffset={5}
>
{content}
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
Radix primitives automatically provide:
/* Style based on state */
[data-state="open"] {
/* Open state styles */
}
[data-state="closed"] {
/* Closed state styles */
}
[data-state="active"] {
/* Active state styles */
}
[data-state="checked"] {
/* Checked state styles */
}
[data-disabled] {
/* Disabled state styles */
}
[data-highlighted] {
/* Keyboard highlighted styles */
}
asChild - Pass your own trigger elementsdata-[state=open] for enter/exit animations