radix-ui

Platform: Web only. For mobile modals/sheets, see the expo-sdk and react-native-patterns skills.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "radix-ui" with this command: npx skills add dralgorhythm/claude-agentic-framework/dralgorhythm-claude-agentic-framework-radix-ui

Radix UI

Platform: Web only. For mobile modals/sheets, see the expo-sdk and react-native-patterns skills.

Overview

Unstyled, accessible UI primitives for React with built-in keyboard navigation, focus management, and ARIA attributes. Designed to be composed with Tailwind CSS and Framer Motion.

Version: Latest (individual packages) or radix-ui unified package

Install (individual packages):

pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-select @radix-ui/react-tooltip @radix-ui/react-tabs

Install (unified package):

pnpm add radix-ui

The unified radix-ui package bundles all primitives - use this for simpler dependency management.

Workflows

Adding a Dialog:

  • Install: pnpm add @radix-ui/react-dialog

  • Import Dialog parts: Root, Trigger, Portal, Overlay, Content

  • Wrap Overlay and Content in Portal for proper stacking

  • Style with Tailwind and data-[state=] selectors

  • Test keyboard navigation (Esc to close, Tab trap)

  • Add Framer Motion animations if needed

Adding a Select:

  • Install: pnpm add @radix-ui/react-select

  • Import Select parts: Root, Trigger, Portal, Content, Item

  • Add Icon and Value to Trigger for visual feedback

  • Style open/closed states with data-[state=open]

  • Test keyboard (Arrow keys, Enter, Type-ahead)

  • Ensure proper z-index for Portal

Adding Tooltips:

  • Install: pnpm add @radix-ui/react-tooltip

  • Wrap app with TooltipProvider

  • Compose Trigger and Content for each tooltip

  • Set delayDuration for hover timing

  • Style with Tailwind arrows using data-[side=]

  • Verify screen reader announcements

Dialog (Modal)

Basic Modal Pattern

// Individual package import import * as Dialog from '@radix-ui/react-dialog';

// OR unified package import // import { Dialog } from 'radix-ui';

function ModalExample() { return ( <Dialog.Root> <Dialog.Trigger asChild> <button className="px-4 py-2 bg-blue-600 text-white rounded"> Open Dialog </button> </Dialog.Trigger>

  &#x3C;Dialog.Portal>
    &#x3C;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" />

    &#x3C;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 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]">
      &#x3C;Dialog.Title className="text-lg font-semibold mb-4">
        Dialog Title
      &#x3C;/Dialog.Title>

      &#x3C;Dialog.Description className="text-sm text-gray-600 mb-4">
        Make changes to your profile here. Click save when you're done.
      &#x3C;/Dialog.Description>

      &#x3C;div className="space-y-4">
        {/* Form content */}
        &#x3C;input type="text" className="w-full px-3 py-2 border rounded" />
      &#x3C;/div>

      &#x3C;div className="flex justify-end gap-2 mt-6">
        &#x3C;Dialog.Close asChild>
          &#x3C;button className="px-4 py-2 border rounded">Cancel&#x3C;/button>
        &#x3C;/Dialog.Close>
        &#x3C;button className="px-4 py-2 bg-blue-600 text-white rounded">
          Save changes
        &#x3C;/button>
      &#x3C;/div>
    &#x3C;/Dialog.Content>
  &#x3C;/Dialog.Portal>
&#x3C;/Dialog.Root>

); }

Controlled Dialog

import { useState } from 'react'; import * as Dialog from '@radix-ui/react-dialog';

function ControlledDialog() { const [open, setOpen] = useState(false);

const handleSubmit = () => { // Process form setOpen(false); };

return ( <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Trigger asChild> <button>Open</button> </Dialog.Trigger> {/* Portal, Overlay, Content... */} </Dialog.Root> ); }

Dialog with Framer Motion

import * as Dialog from '@radix-ui/react-dialog'; import { motion, AnimatePresence } from 'framer-motion';

function AnimatedDialog() { const [open, setOpen] = useState(false);

return ( <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Trigger asChild> <button>Open</button> </Dialog.Trigger>

  &#x3C;AnimatePresence>
    {open &#x26;&#x26; (
      &#x3C;Dialog.Portal forceMount>
        &#x3C;Dialog.Overlay asChild>
          &#x3C;motion.div
            className="fixed inset-0 bg-black/50"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          />
        &#x3C;/Dialog.Overlay>

        &#x3C;Dialog.Content asChild>
          &#x3C;motion.div
            className="fixed left-[50%] top-[50%] max-w-[500px] rounded-lg bg-white p-6"
            initial={{ opacity: 0, scale: 0.95, x: '-50%', y: '-50%' }}
            animate={{ opacity: 1, scale: 1, x: '-50%', y: '-50%' }}
            exit={{ opacity: 0, scale: 0.95, x: '-50%', y: '-50%' }}
            transition={{ duration: 0.2 }}
          >
            {/* Content */}
          &#x3C;/motion.div>
        &#x3C;/Dialog.Content>
      &#x3C;/Dialog.Portal>
    )}
  &#x3C;/AnimatePresence>
&#x3C;/Dialog.Root>

); }

Select (Dropdown)

Basic 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 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 data-[placeholder]:text-gray-400 min-w-[200px]"> <Select.Value placeholder="Select a fruit..." /> <Select.Icon> <ChevronDownIcon /> </Select.Icon> </Select.Trigger>

  &#x3C;Select.Portal>
    &#x3C;Select.Content className="overflow-hidden bg-white rounded-md shadow-lg border">
      &#x3C;Select.Viewport className="p-1">
        &#x3C;Select.Item value="apple" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 focus:bg-blue-100 outline-none cursor-pointer data-[disabled]:opacity-50 data-[disabled]:pointer-events-none">
          &#x3C;Select.ItemIndicator className="absolute left-2">
            &#x3C;CheckIcon />
          &#x3C;/Select.ItemIndicator>
          &#x3C;Select.ItemText>Apple&#x3C;/Select.ItemText>
        &#x3C;/Select.Item>

        &#x3C;Select.Item value="banana" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 focus:bg-blue-100 outline-none cursor-pointer">
          &#x3C;Select.ItemIndicator className="absolute left-2">
            &#x3C;CheckIcon />
          &#x3C;/Select.ItemIndicator>
          &#x3C;Select.ItemText>Banana&#x3C;/Select.ItemText>
        &#x3C;/Select.Item>

        &#x3C;Select.Item value="orange" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 focus:bg-blue-100 outline-none cursor-pointer">
          &#x3C;Select.ItemIndicator className="absolute left-2">
            &#x3C;CheckIcon />
          &#x3C;/Select.ItemIndicator>
          &#x3C;Select.ItemText>Orange&#x3C;/Select.ItemText>
        &#x3C;/Select.Item>
      &#x3C;/Select.Viewport>
    &#x3C;/Select.Content>
  &#x3C;/Select.Portal>
&#x3C;/Select.Root>

); }

Grouped Select with Labels

<Select.Root> <Select.Trigger>{/* ... */}</Select.Trigger>

<Select.Portal> <Select.Content> <Select.Viewport> <Select.Group> <Select.Label className="px-8 py-2 text-xs font-semibold text-gray-500"> Fruits </Select.Label> <Select.Item value="apple">{/* ... /}</Select.Item> <Select.Item value="banana">{/ ... */}</Select.Item> </Select.Group>

    &#x3C;Select.Separator className="h-px bg-gray-200 my-1" />

    &#x3C;Select.Group>
      &#x3C;Select.Label className="px-8 py-2 text-xs font-semibold text-gray-500">
        Vegetables
      &#x3C;/Select.Label>
      &#x3C;Select.Item value="carrot">{/* ... */}&#x3C;/Select.Item>
      &#x3C;Select.Item value="broccoli">{/* ... */}&#x3C;/Select.Item>
    &#x3C;/Select.Group>
  &#x3C;/Select.Viewport>
&#x3C;/Select.Content>

</Select.Portal> </Select.Root>

Tooltip

Basic Tooltip

import * as Tooltip from '@radix-ui/react-tooltip';

// Wrap your app once function App() { return ( <Tooltip.Provider delayDuration={200}> <YourApp /> </Tooltip.Provider> ); }

// Use in components function TooltipExample() { return ( <Tooltip.Root> <Tooltip.Trigger asChild> <button className="px-4 py-2 bg-gray-100 rounded"> Hover me </button> </Tooltip.Trigger>

  &#x3C;Tooltip.Portal>
    &#x3C;Tooltip.Content
      className="bg-gray-900 text-white text-sm px-3 py-2 rounded shadow-lg max-w-xs data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:zoom-in-95"
      sideOffset={5}
    >
      This is a helpful tooltip
      &#x3C;Tooltip.Arrow className="fill-gray-900" />
    &#x3C;/Tooltip.Content>
  &#x3C;/Tooltip.Portal>
&#x3C;/Tooltip.Root>

); }

Tooltip with Dynamic Positioning

<Tooltip.Content side="top" // top | right | bottom | left align="center" // start | center | end sideOffset={5} className="bg-gray-900 text-white px-3 py-2 rounded"

Content <Tooltip.Arrow className="fill-gray-900" /> </Tooltip.Content>

DropdownMenu

Basic Dropdown Menu

import * as DropdownMenu from '@radix-ui/react-dropdown-menu';

function DropdownExample() { return ( <DropdownMenu.Root> <DropdownMenu.Trigger asChild> <button className="px-4 py-2 bg-white border rounded hover:bg-gray-50"> Options </button> </DropdownMenu.Trigger>

  &#x3C;DropdownMenu.Portal>
    &#x3C;DropdownMenu.Content
      className="min-w-[220px] bg-white rounded-md shadow-lg border p-1"
      sideOffset={5}
    >
      &#x3C;DropdownMenu.Item className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-blue-50 focus:bg-blue-100 outline-none">
        New Tab
        &#x3C;span className="ml-auto text-xs text-gray-500">⌘T&#x3C;/span>
      &#x3C;/DropdownMenu.Item>

      &#x3C;DropdownMenu.Item className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-blue-50 focus:bg-blue-100 outline-none">
        New Window
        &#x3C;span className="ml-auto text-xs text-gray-500">⌘N&#x3C;/span>
      &#x3C;/DropdownMenu.Item>

      &#x3C;DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

      &#x3C;DropdownMenu.Item
        className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-red-50 focus:bg-red-100 text-red-600 outline-none"
        onSelect={() => console.log('Delete')}
      >
        Delete
      &#x3C;/DropdownMenu.Item>
    &#x3C;/DropdownMenu.Content>
  &#x3C;/DropdownMenu.Portal>
&#x3C;/DropdownMenu.Root>

); }

Dropdown with Checkboxes and Radio Groups

<DropdownMenu.Content> <DropdownMenu.CheckboxItem checked={showBookmarks} onCheckedChange={setShowBookmarks} className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-blue-50 outline-none"

&#x3C;DropdownMenu.ItemIndicator className="mr-2">
  &#x3C;CheckIcon />
&#x3C;/DropdownMenu.ItemIndicator>
Show Bookmarks

</DropdownMenu.CheckboxItem>

<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

<DropdownMenu.RadioGroup value={view} onValueChange={setView}> <DropdownMenu.RadioItem value="grid" className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-blue-50 outline-none" > <DropdownMenu.ItemIndicator className="mr-2"> <DotFilledIcon /> </DropdownMenu.ItemIndicator> Grid View </DropdownMenu.RadioItem>

&#x3C;DropdownMenu.RadioItem value="list" className="flex items-center px-3 py-2 text-sm rounded cursor-pointer hover:bg-blue-50 outline-none">
  &#x3C;DropdownMenu.ItemIndicator className="mr-2">
    &#x3C;DotFilledIcon />
  &#x3C;/DropdownMenu.ItemIndicator>
  List View
&#x3C;/DropdownMenu.RadioItem>

</DropdownMenu.RadioGroup> </DropdownMenu.Content>

Tabs

Basic Tabs

import * as Tabs from '@radix-ui/react-tabs';

function TabsExample() { return ( <Tabs.Root defaultValue="tab1" className="w-full"> <Tabs.List className="flex border-b"> <Tabs.Trigger value="tab1" className="px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:text-blue-600 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" > Account </Tabs.Trigger>

    &#x3C;Tabs.Trigger
      value="tab2"
      className="px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:text-blue-600 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    >
      Password
    &#x3C;/Tabs.Trigger>

    &#x3C;Tabs.Trigger
      value="tab3"
      className="px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:text-blue-600 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    >
      Settings
    &#x3C;/Tabs.Trigger>
  &#x3C;/Tabs.List>

  &#x3C;Tabs.Content value="tab1" className="p-4">
    &#x3C;h3 className="text-lg font-semibold mb-2">Account Settings&#x3C;/h3>
    &#x3C;p className="text-gray-600">Manage your account details here.&#x3C;/p>
  &#x3C;/Tabs.Content>

  &#x3C;Tabs.Content value="tab2" className="p-4">
    &#x3C;h3 className="text-lg font-semibold mb-2">Password Settings&#x3C;/h3>
    &#x3C;p className="text-gray-600">Change your password here.&#x3C;/p>
  &#x3C;/Tabs.Content>

  &#x3C;Tabs.Content value="tab3" className="p-4">
    &#x3C;h3 className="text-lg font-semibold mb-2">General Settings&#x3C;/h3>
    &#x3C;p className="text-gray-600">Configure application settings.&#x3C;/p>
  &#x3C;/Tabs.Content>
&#x3C;/Tabs.Root>

); }

Vertical Tabs

<Tabs.Root defaultValue="tab1" orientation="vertical" className="flex gap-4"> <Tabs.List className="flex flex-col gap-1 border-r pr-4"> <Tabs.Trigger value="tab1" className="px-4 py-2 text-left text-sm rounded hover:bg-gray-100 data-[state=active]:bg-blue-100 data-[state=active]:text-blue-700" > Profile </Tabs.Trigger> <Tabs.Trigger value="tab2" className="px-4 py-2 text-left text-sm rounded hover:bg-gray-100 data-[state=active]:bg-blue-100 data-[state=active]:text-blue-700" > Billing </Tabs.Trigger> </Tabs.List>

<div className="flex-1"> <Tabs.Content value="tab1">{/* ... /}</Tabs.Content> <Tabs.Content value="tab2">{/ ... */}</Tabs.Content> </div> </Tabs.Root>

Styling with Tailwind

Data Attribute Selectors

// State-based styling className="data-[state=open]:bg-blue-50 data-[state=closed]:bg-gray-50"

// Side-based styling (for positioned elements) className="data-[side=top]:animate-slide-down data-[side=bottom]:animate-slide-up"

// Disabled state className="data-[disabled]:opacity-50 data-[disabled]:pointer-events-none"

// Highlighted state (for keyboard navigation) className="data-[highlighted]:bg-blue-100"

// Checked state className="data-[state=checked]:bg-blue-600"

Common Tailwind Patterns

// Focus ring (keyboard navigation) className="outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"

// Backdrop overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm"

// Centered modal className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]"

// Dropdown content z-index className="z-50 bg-white rounded-md shadow-lg"

// Smooth transitions className="transition-colors duration-150"

Accessibility

Keyboard Navigation

All Radix components handle keyboard navigation automatically:

  • Dialog: Esc to close, Tab trap inside modal

  • Select: Arrow keys to navigate, Enter to select, type-ahead search

  • DropdownMenu: Arrow keys to navigate, Enter to select

  • Tabs: Arrow keys to switch tabs, Home/End for first/last

  • Tooltip: Focus trigger shows tooltip

ARIA Attributes

Radix components automatically add proper ARIA attributes:

// Dialog adds: // role="dialog" // aria-labelledby (references Dialog.Title) // aria-describedby (references Dialog.Description)

// Select adds: // role="combobox" // aria-expanded // aria-controls

// Always provide Dialog.Title and Dialog.Description <Dialog.Content> <Dialog.Title>Required for a11y</Dialog.Title> <Dialog.Description>Screen readers announce this</Dialog.Description> </Dialog.Content>

Focus Management

// Auto-focus on mount <Dialog.Content onOpenAutoFocus={(e) => { e.preventDefault(); // Prevent default focus customElementRef.current?.focus(); // Custom focus target }}>

// Focus on close <Dialog.Content onCloseAutoFocus={(e) => { e.preventDefault(); triggerRef.current?.focus(); }}>

Controlled vs Uncontrolled

Uncontrolled (Default)

// Component manages its own state <Dialog.Root defaultOpen={false}> <Dialog.Trigger>Open</Dialog.Trigger> {/* ... */} </Dialog.Root>

<Select.Root defaultValue="apple"> <Select.Trigger>{/* ... */}</Select.Trigger> </Select.Root>

Controlled (Recommended for Complex UIs)

// Parent manages state const [open, setOpen] = useState(false); const [value, setValue] = useState('');

<Dialog.Root open={open} onOpenChange={setOpen}> {/* ... */} </Dialog.Root>

<Select.Root value={value} onValueChange={setValue}> {/* ... */} </Select.Root>

Portal Usage

Why Use Portals

Portals render components outside the DOM hierarchy to avoid:

  • z-index conflicts

  • overflow: hidden clipping

  • CSS transform issues

// Without portal (may be clipped) <Dialog.Content>{/* ... */}</Dialog.Content>

// With portal (renders at document.body) <Dialog.Portal> <Dialog.Content>{/* ... */}</Dialog.Content> </Dialog.Portal>

// Custom portal container <Dialog.Portal container={customContainerRef.current}> <Dialog.Content>{/* ... */}</Dialog.Content> </Dialog.Portal>

Portal Best Practices

// Always portal Overlay and Content together <Dialog.Portal> <Dialog.Overlay /> <Dialog.Content /> </Dialog.Portal>

// Use forceMount with AnimatePresence <AnimatePresence> {open && ( <Dialog.Portal forceMount> {/* Framer Motion components */} </Dialog.Portal> )} </AnimatePresence>

Best Practices

  • Use asChild prop to compose with your own elements without wrapper divs

  • Always Portal overlays and dropdowns to avoid z-index issues

  • Provide Title and Description for Dialogs (accessibility requirement)

  • Use data-[state=] selectors for styling open/closed states

  • Prefer controlled components for complex state management

  • Add focus rings with Tailwind outline-none + focus:ring-2

  • Use TooltipProvider once at app root, not per tooltip

  • Combine with Framer Motion using asChild and forceMount

  • Test keyboard navigation for all interactive components

  • Set proper sideOffset (usually 5-10px) for floating elements

  • Use consistent styling patterns across all Radix components

Anti-Patterns

  • ❌ Forgetting Dialog.Portal (causes z-index issues)

  • ❌ Missing Dialog.Title or Dialog.Description (fails a11y)

  • ❌ Not using asChild with custom triggers (creates wrapper divs)

  • ❌ Hardcoding colors instead of using data-[state=] selectors

  • ❌ Multiple TooltipProviders (unnecessary overhead)

  • ❌ Blocking onSelect propagation without e.preventDefault()

  • ❌ Forgetting focus:ring styles (poor keyboard UX)

  • ❌ Not testing with keyboard navigation

  • ❌ Using controlled without onOpenChange/onValueChange

  • ❌ Mixing controlled and uncontrolled patterns

Feedback Loops

Accessibility testing:

Test with keyboard only (no mouse)

Tab through all interactive elements

Esc should close Dialogs, Dropdowns, Selects

Arrow keys should navigate menus and selects

Screen reader testing:

macOS: VoiceOver (Cmd+F5)

Verify Dialog.Title and Dialog.Description are announced

Verify Select options are announced correctly

Check for proper role attributes

Visual regression:

// Test all states: // - Closed vs Open // - Hover vs Focus // - Selected vs Unselected // - Disabled states // - Different viewport sizes

Integration with Framer Motion:

// Use forceMount to control mounting // Wrap in AnimatePresence for exit animations // Test that focus management still works with animations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

react-native-reanimated

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

cloud-native-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

brainstorming

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

context-management

No summary provided by upstream source.

Repository SourceNeeds Review