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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- radix-ui Installs to ~/.agents/skills/radix-ui/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install radix-ui The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project radix-ui Installs to .agents/skills/radix-ui/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/radix-ui.zipunzip -o radix-ui.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) radix-ui PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- Read
AGENTS.mdandpackage.json. Determine whether the project uses the unifiedradix-uipackage or individual@radix-ui/react-*packages, and the React version. - Check if shadcn/ui is present (
components.json). If so, extend the existing wrappers incomponents/uiinstead of creating parallel ones ($shadcn-ui). - 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 need | Primitive | Not this |
|---|---|---|
| Modal that blocks the page | Dialog | Popover with an overlay |
| Destructive confirm requiring a choice | AlertDialog | Dialog (it closes on outside click) |
| Floating panel with arbitrary content, non-modal | Popover | Tooltip |
| List of actions/commands from a button | DropdownMenu | Popover with buttons |
| Right-click actions | ContextMenu | |
| App-style top menu bar | Menubar | |
| Site navigation with flyouts | NavigationMenu | DropdownMenu (menu role is for actions) |
| Pick one value from a list (form field) | Select | DropdownMenu |
| Type-to-filter suggestions | Not in Radix; use a combobox ($aria-patterns) or cmdk | Select |
| Short, non-interactive hint on hover/focus | Tooltip | Tooltip with links or buttons inside |
| Rich hover preview for sighted mouse users | HoverCard | Sole carrier of essential info |
| Panels switched by tabs | Tabs | |
| Expand/collapse sections | Accordion (group) / Collapsible (single) | |
| On/off setting | Switch or Checkbox | Toggle (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.Titleis required; Radix warns in the console without it. If the design has no visible title, wrap it inVisuallyHiddenrather than deleting it.- Provide
Dialog.Descriptionor passaria-describedby={undefined}to opt out explicitly. - Keep
Portalunless you have a reason: it escapesoverflow: hiddenand 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
refas a normal prop;forwardRefstill 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
openwhen something outside the trigger opens it or when closing depends on async work. Otherwise leave it uncontrolled. - Prevent closing in specific cases with
onEscapeKeyDown,onPointerDownOutsideoronInteractOutsideandevent.preventDefault(), for example while a save is pending. - Opening a Dialog from a DropdownMenu item: control the dialog’s
openstate and set it from the item’sonSelect; 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 useforceMounton Portal/Content and drive presence yourself. - Popper-based parts (
Popover,Tooltip,DropdownMenu,Selectwithposition="popper") expose--radix-*-available-height/widthandtransform-originvariables; use them to keep content on screen. side,align,sideOffset,collisionPaddingcontrol placement; avoid manual positioning.
Tooltip specifics
- Wrap the app in a single
Tooltip.Provider(setdelayDurationthere). - Disabled buttons do not fire pointer events; wrap the trigger in a
spanwithtabIndex={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-labeltoo.
Gotchas
z-indexfights: portaled content goes to the end ofbody. Use one z-index scale token for overlays; do not raise numbers per component.bodygetspointer-events: nonewhile 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-righttobody; fixed headers jump unless they also account for the scrollbar gap (scrollbar-gutter: stableonhtmlavoids it). - Hydration warnings about mismatched IDs come from rendering different trees on server and
client; Radix uses
useIdand is stable when the tree is. Select.Valueshows the selected item’s text; items need a non-emptyvaluestring.- Mixing
radix-uiand 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-testingchecks (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.yaml
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.