radix-ui

SKILL.md

Radix UI - Accessible Headless Components

Build accessible design systems with unstyled, composable primitives


When to Use This Skill

Use this skill when:

  • Building accessible UI components from scratch
  • Working with shadcn/ui (which uses Radix primitives)
  • Need keyboard navigation and ARIA support built-in
  • Creating custom-styled dialogs, dropdowns, popovers
  • Building design systems with consistent accessibility
  • Want headless components that you can style freely

Don't use this skill when:

  • Need pre-styled components (use Material-UI, Chakra UI)
  • Building static content without interaction
  • Simple HTML elements are sufficient (<button>, <select>)
  • Working with non-React frameworks

Critical Patterns

Pattern 1: Composition with asChild

When: Passing custom components as triggers or content

Good:

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

<Dialog.Root>
  <Dialog.Trigger asChild>
    <Button variant="primary">Open</Button>
  </Dialog.Trigger>
</Dialog.Root>

Bad:

// ❌ Wrapper divs break accessibility
<Dialog.Root>
  <div onClick={() => setOpen(true)}>
    <Button>Open</Button>
  </div>
</Dialog.Root>

Why: asChild merges props into your component, maintaining accessibility attributes and event handlers without wrapper elements.


Pattern 2: Portal for Overlays

When: Rendering modals, dialogs, popovers that need to escape parent containers

Good:

<Dialog.Root>
  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
      {/* Content */}
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Bad:

// ❌ No portal - overlay affected by parent overflow/z-index
<Dialog.Root>
  <Dialog.Overlay />
  <Dialog.Content>
    {/* Content */}
  </Dialog.Content>
</Dialog.Root>

Why: Portal renders overlay at document root, avoiding z-index issues and overflow clipping.


Pattern 3: Controlled vs Uncontrolled State

When: Managing open/close state

Good (Controlled):

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

<Dialog.Root open={open} onOpenChange={setOpen}>
  {/* Now you can control externally */}
  <Dialog.Trigger asChild>
    <button>Open Dialog</button>
  </Dialog.Trigger>
</Dialog.Root>

// Programmatic control
<button onClick={() => setOpen(true)}>Open from outside</button>

Good (Uncontrolled):

// Radix manages state internally
<Dialog.Root>
  <Dialog.Trigger asChild>
    <button>Open Dialog</button>
  </Dialog.Trigger>
</Dialog.Root>

When to use each:

  • Controlled: Need external control, analytics, complex state
  • Uncontrolled: Simple toggle, no external dependencies

Pattern 4: Styling with Tailwind

When: Applying styles to headless components

Good:

<DropdownMenu.Content
  className="min-w-[220px] bg-white rounded-md shadow-lg p-1"
  sideOffset={5}
>
  <DropdownMenu.Item className="px-3 py-2 rounded hover:bg-gray-100 cursor-pointer outline-none">
    Profile
  </DropdownMenu.Item>
</DropdownMenu.Content>

Bad:

// ❌ Inline styles, no focus states
<DropdownMenu.Content style={{ background: 'white', padding: '4px' }}>
  <DropdownMenu.Item style={{ padding: '8px' }}>
    Profile
  </DropdownMenu.Item>
</DropdownMenu.Content>

Why: Tailwind classes handle hover, focus, and responsive states. Always include outline-none and visible focus states for keyboard users.


Anti-Patterns

❌ Anti-Pattern 1: Breaking Composition Structure

Don't do this:

// ❌ Adding wrapper divs breaks keyboard navigation
<Dialog.Root>
  <div className="wrapper">
    <Dialog.Trigger>Open</Dialog.Trigger>
  </div>
  <div className="overlay-wrapper">
    <Dialog.Overlay />
  </div>
</Dialog.Root>

Why it's bad: Extra wrappers interfere with Radix's accessibility features, ARIA relationships, and keyboard navigation.

Do this instead:

// ✅ Keep Radix components as direct children
<Dialog.Root>
  <Dialog.Trigger className="custom-styles">Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className="custom-overlay-styles" />
  </Dialog.Portal>
</Dialog.Root>

❌ Anti-Pattern 2: Ignoring Portal

Don't do this:

// ❌ No portal - will be clipped by overflow:hidden parents
<Dialog.Root>
  <Dialog.Overlay className="fixed inset-0 bg-black/50" />
  <Dialog.Content className="fixed top-1/2 left-1/2">
    Content
  </Dialog.Content>
</Dialog.Root>

Why it's bad: Parent containers with overflow: hidden will clip your modal, making it invisible.

Do this instead:

// ✅ Always use Portal for overlays
<Dialog.Root>
  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2">
      Content
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

❌ Anti-Pattern 3: Missing Keyboard Navigation Styles

Don't do this:

// ❌ No visible focus state
<DropdownMenu.Item className="px-3 py-2 hover:bg-gray-100">
  Profile
</DropdownMenu.Item>

Why it's bad: Keyboard users can't see which item is focused, breaking accessibility.

Do this instead:

// ✅ Include focus-visible styles
<DropdownMenu.Item className="px-3 py-2 hover:bg-gray-100 focus-visible:bg-blue-100 focus-visible:outline-none">
  Profile
</DropdownMenu.Item>

❌ Anti-Pattern 4: Not Using asChild for Custom Components

Don't do this:

// ❌ Wrapper button loses Radix functionality
<Dialog.Trigger>
  <Button>Open</Button>
</Dialog.Trigger>

Why it's bad: Creates nested buttons (invalid HTML) and duplicates event handlers.

Do this instead:

// ✅ asChild merges props into Button
<Dialog.Trigger asChild>
  <Button>Open</Button>
</Dialog.Trigger>

❌ Anti-Pattern 5: Hardcoding Animations

Don't do this:

// ❌ Animations not synchronized with open state
const [open, setOpen] = useState(false);

<Dialog.Content className={open ? 'animate-in' : 'animate-out'}>
  Content
</Dialog.Content>

Why it's bad: Animation timing doesn't match Radix's state transitions.

Do this instead:

// ✅ Use data attributes for state-based styling
<Dialog.Content className="data-[state=open]:animate-in data-[state=closed]:animate-out">
  Content
</Dialog.Content>

What This Skill Covers

  • Primitive components (Dialog, Dropdown, Popover, etc.)
  • Accessibility built-in (ARIA, keyboard navigation)
  • Integration with Tailwind CSS and shadcn/ui

For all components, see references/.


Dialog

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

export function DialogDemo() {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <button>Open Dialog</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 bg-white p-6 rounded-lg">
          <Dialog.Title>Edit Profile</Dialog.Title>
          <Dialog.Description>Make changes here.</Dialog.Description>
          
          <Dialog.Close asChild>
            <button>Close</button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Dropdown Menu

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

export function DropdownDemo() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger>Options</DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content className="bg-white rounded shadow-lg p-1">
          <DropdownMenu.Item className="px-3 py-2">Profile</DropdownMenu.Item>
          <DropdownMenu.Item className="px-3 py-2">Settings</DropdownMenu.Item>
          <DropdownMenu.Separator className="h-px bg-gray-200" />
          <DropdownMenu.Item className="px-3 py-2 text-red-600">Logout</DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

Quick Reference

// Dialog
<Dialog.Root>
  <Dialog.Trigger />
  <Dialog.Portal>
    <Dialog.Overlay />
    <Dialog.Content>
      <Dialog.Title />
      <Dialog.Description />
      <Dialog.Close />
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

// Dropdown
<DropdownMenu.Root>
  <DropdownMenu.Trigger />
  <DropdownMenu.Content>
    <DropdownMenu.Item />
  </DropdownMenu.Content>
</DropdownMenu.Root>

Learn More


External References


Maintained by dsmj-ai-toolkit

Weekly Installs
2
First Seen
Feb 25, 2026
Installed on
trae-cn2
gemini-cli2
codebuddy2
github-copilot2
codex2
kiro-cli2