asChildWhen to use: swapping a part's rendered element (e.g. Tooltip.Trigger as an <a> instead of <button>), or composing Radix's behavior onto your own design-system components.
How: pass asChild to the part and wrap your own single child. Your component must spread all props onto its DOM node and forward its ref (React.forwardRef), or Radix's injected behavior breaks silently.
Trade-offs: full control over markup, but you inherit responsibility for keeping the swapped element accessible and functional.
When to use: any time you'll reuse a primitive more than once — Dialog, DropdownMenu, Select, etc.
How: abstract the boilerplate parts (e.g. Overlay + Close for Dialog) into your own wrapper component, exporting a smaller surface (Dialog, DialogTrigger, DialogContent) that composes the primitive parts internally.
Trade-offs: one-time setup cost per primitive, but every call site becomes far simpler and consistent.
data-stateWhen to use: any stateful primitive (Accordion, Checkbox, Toggle, Tabs, Dialog, etc.).
How: target [data-state="..."] in CSS or CSS-in-JS instead of tracking state in JS and toggling classes manually.
Trade-offs: none — this is the idiomatic, zero-JS-overhead approach the whole library is designed around.
When to use: animating open/close height without height: auto's inability to transition.
How: use the exposed --radix-*-content-width/-height CSS variables in a @keyframes animation keyed off data-state.
Trade-offs: CSS-only, no JS measurement code needed; works for both mount and unmount since Radix suspends unmount until the animation completes.
When to use: animations CSS keyframes can't express well (spring physics, layout animation, gesture-driven exits).
How: set forceMount on the relevant Content/Indicator part and drive its actual presence from the animation library's own transition state (e.g. useTransition from react-spring).
Trade-offs: more setup than CSS keyframes; you take over mount/unmount timing entirely.
When to use: Popover, DropdownMenu, ContextMenu, Select (popper mode), HoverCard, Tooltip, NavigationMenu.
How: configure side/align/sideOffset/alignOffset plus avoidCollisions/collisionBoundary/collisionPadding/sticky/hideWhenDetached on Content; use the exposed --radix-*-content-transform-origin and -available-width/-height CSS variables for origin-aware animation and size constraints.
Trade-offs: shared API across many primitives means the technique transfers directly once learned once.
When to use: Menu family (Item/CheckboxItem/RadioItem/SubTrigger), RadioGroup, Tabs, ToggleGroup, Toolbar.
How: rely on Radix's built-in single-tab-stop-plus-arrow-keys behavior; configure loop/orientation/dir rather than implementing focus movement yourself.
Trade-offs: none if you stay within the primitive's own item parts — reimplementing this by hand is exactly the hard part Radix exists to solve.
When to use: Checkbox, RadioGroup, Switch, Slider inside a native <form>.
How: rely on the automatically-rendered hidden native input (default behavior); only reach for the unstable_Provider/unstable_Trigger/unstable_BubbleInput parts (Checkbox/Switch) if you need to relocate or exclude that input.
Trade-offs: default path needs zero extra code; the decoupled path is explicitly unstable API.
When to use: any app needing right-to-left layout/keyboard behavior.
How: wrap the app once in Direction.Provider with dir="rtl" instead of passing dir to every individual primitive instance.
Trade-offs: per-instance dir overrides still work for exceptions.