Patterns

Patterns — Radix Primitives

Composing with asChild

When 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.

Building your own component API

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.

Styling via data-state

When 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.

Animating content size (Accordion/Collapsible)

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.

Delegating to a JS animation library

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.

Positioned overlay content

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.

Roving tabindex collections

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.

Form-compatible stateful controls

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.

RTL support

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.