Radix UI Primitives skill for Codex

Accessible behavior, your own styling. Codex builds design-system components on Radix Primitives without breaking what makes them accessible. It picks the primitive that matches the interaction (menu versus select versus popover), keeps required parts like Dialog.Title, forwards props and refs so asChild works, styles via data-state attributes and Radix CSS variables, and fixes the usual portal, z-index and stuck pointer-events problems.

  • Frontend
  • $radix-ui
  • 165 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- radix-ui

Installs to ~/.agents/skills/radix-ui/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $radix-ui, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds accessible custom React components on Radix Primitives (the radix-ui package) - Dialog, Popover, DropdownMenu, Select, Tabs, Tooltip, Accordion and friends - with correct anatomy, asChild composition, controlled state, portals, data-state styling and animation hooks. Use when a user needs a styled dialog, menu, combobox-like select or tooltip that must be keyboard and screen-reader correct, wraps Radix in a design-system component, or debugs focus traps, z-index and portal issues, hydration warnings or missing Dialog titles.

Use it when

  • You need accessible overlays, menus, tabs or selects with custom styling
  • A design system is wrapping Radix primitives
  • Radix components show focus, portal, z-index or hydration problems

Use something else when

  • The project already uses shadcn/ui wrappers you should extend: use shadcn-ui
  • You need a pattern Radix lacks, such as an editable combobox: use aria-patterns

How to check the result

Operate every component with the keyboard alone, confirm focus returns to the trigger on close, check the console for Radix warnings, and run axe against the open state.

The mistake it prevents

Wrapping a trigger child that does not spread props or forward its ref, so asChild silently loses Radix's event handlers and ARIA attributes.

~/.agents/skills/radix-ui/SKILL.md

--- name: radix-ui description: Builds accessible custom React components on Radix Primitives (the radix-ui package) - Dialog, Popover, DropdownMenu, Select, Tabs, Tooltip,… license: MIT ---

Done well means: each design-system component wraps one Radix primitive with its full anatomy intact, forwards refs and props so asChild keeps working, exposes a small typed API, and inherits Radix’s keyboard and ARIA behavior instead of re-implementing or breaking it.

Before writing anything

  1. Read AGENTS.md and package.json. Determine whether the project uses the unified radix-ui package or individual @radix-ui/react-* packages, and the React version.
  2. Check if shadcn/ui is present (components.json). If so, extend the existing wrappers in components/ui instead of creating parallel ones ($shadcn-ui).
  3. Install with npm install radix-ui (network needed; ask for approval in a sandbox). Imports: import { Dialog, DropdownMenu } from "radix-ui".

Choose the primitive

UI needPrimitiveNot this
Modal that blocks the pageDialogPopover with an overlay
Destructive confirm requiring a choiceAlertDialogDialog (it closes on outside click)
Floating panel with arbitrary content, non-modalPopoverTooltip
List of actions/commands from a buttonDropdownMenuPopover with buttons
Right-click actionsContextMenu
App-style top menu barMenubar
Site navigation with flyoutsNavigationMenuDropdownMenu (menu role is for actions)
Pick one value from a list (form field)SelectDropdownMenu
Type-to-filter suggestionsNot in Radix; use a combobox ($aria-patterns) or cmdkSelect
Short, non-interactive hint on hover/focusTooltipTooltip with links or buttons inside
Rich hover preview for sighted mouse usersHoverCardSole carrier of essential info
Panels switched by tabsTabs
Expand/collapse sectionsAccordion (group) / Collapsible (single)
On/off settingSwitch or CheckboxToggle (pressed button state)

Anatomy: keep every part

import { Dialog } from "radix-ui";

export function EditProfileDialog({ trigger, children }: Props) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="dialog-overlay" />
        <Dialog.Content className="dialog-content" aria-describedby={undefined}>
          <Dialog.Title>Edit profile</Dialog.Title>
          {children}
          <Dialog.Close asChild>
            <button aria-label="Close">×</button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
  • Dialog.Title is required; Radix warns in the console without it. If the design has no visible title, wrap it in VisuallyHidden rather than deleting it.
  • Provide Dialog.Description or pass aria-describedby={undefined} to opt out explicitly.
  • Keep Portal unless you have a reason: it escapes overflow: hidden and stacking contexts.

asChild and composition

asChild merges Radix props and behavior onto your child element instead of rendering its own.

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
  <button ref={ref} {...props} className={cx("btn", props.className)} />
));

<Tooltip.Trigger asChild><Button>Save</Button></Tooltip.Trigger>

Rules for components used under asChild:

  • Spread all incoming props onto the DOM element (event handlers, data-state, aria-*).
  • Forward the ref (React 19 can take ref as a normal prop; forwardRef still works).
  • Render exactly one child element; fragments and strings break the merge.
  • Nest triggers by chaining asChild: <Tooltip.Trigger asChild><Dialog.Trigger asChild><Button/>....

Controlled state

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

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

async function onSubmit(data: FormValues) {
  await save(data);
  setOpen(false);              // close only after success
}
  • Use controlled open when something outside the trigger opens it or when closing depends on async work. Otherwise leave it uncontrolled.
  • Prevent closing in specific cases with onEscapeKeyDown, onPointerDownOutside or onInteractOutside and event.preventDefault(), for example while a save is pending.
  • Opening a Dialog from a DropdownMenu item: control the dialog’s open state and set it from the item’s onSelect; render the Dialog outside the menu content, or the dialog unmounts when the menu closes. modal={false} on the menu also avoids pointer-events locking issues.

Styling and animation

Radix renders state as data attributes; style them instead of tracking classes in React.

.dialog-content[data-state="open"] { animation: dialog-in 160ms ease-out; }
.dialog-content[data-state="closed"] { animation: dialog-out 120ms ease-in; }
.popover-content { transform-origin: var(--radix-popover-content-transform-origin); }
.select-content { max-height: var(--radix-select-content-available-height); }
[data-disabled] { opacity: 0.5; pointer-events: none; }
  • Radix waits for CSS exit animations (on data-state="closed") before unmounting. For JS animation libraries use forceMount on Portal/Content and drive presence yourself.
  • Popper-based parts (Popover, Tooltip, DropdownMenu, Select with position="popper") expose --radix-*-available-height/width and transform-origin variables; use them to keep content on screen.
  • side, align, sideOffset, collisionPadding control placement; avoid manual positioning.

Tooltip specifics

  • Wrap the app in a single Tooltip.Provider (set delayDuration there).
  • Disabled buttons do not fire pointer events; wrap the trigger in a span with tabIndex={0}, or better, explain why the action is unavailable in visible text.
  • Tooltip content is announced as a description; never put the only label of an icon button in a tooltip alone. Give the button an aria-label too.

Gotchas

  • z-index fights: portaled content goes to the end of body. Use one z-index scale token for overlays; do not raise numbers per component.
  • body gets pointer-events: none while a modal is open; if it sticks after closing, two modal layers closed in the wrong order (usually Dialog opened from a DropdownMenu). Fix the state flow rather than forcing styles.
  • Scroll lock adds padding-right to body; fixed headers jump unless they also account for the scrollbar gap (scrollbar-gutter: stable on html avoids it).
  • Hydration warnings about mismatched IDs come from rendering different trees on server and client; Radix uses useId and is stable when the tree is.
  • Select.Value shows the selected item’s text; items need a non-empty value string.
  • Mixing radix-ui and old @radix-ui/react-* packages yields two copies of context; one provider will not see consumers from the other.

Verification

  • Keyboard only: open with Enter/Space, navigate with arrows (menus, tabs, select), close with Escape, focus returns to the trigger.
  • Screen reader smoke test (VoiceOver or NVDA): dialog announces its title; menu items announce role and position.
  • No Radix warnings in the console (missing Title/Description).
  • Inside a scroll container and near viewport edges, popper content flips and stays visible.
  • Run $accessibility-testing checks (axe) on the open state, not only the closed one.

Output

List each wrapper component, the primitive it uses, its public props, controlled vs uncontrolled behavior, and any deviation from Radix defaults with the reason.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Radix UI Primitives"
  short_description: "Accessible behavior, your own styling"
  default_prompt: "$radix-ui Build our Dialog, Popover and DropdownMenu components on Radix with our styles"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc