Overview
Composability is at the heart of Radix UI Primitives. Every component is designed to compose naturally, just like native HTML elements, giving you complete control over the DOM structure and behavior.Radix components follow a 1-to-1 mapping principle: each component renders exactly one DOM element (unless explicitly documented otherwise).
1-to-1 DOM Mapping
The fundamental principle of composability in Radix is that each component maps to a single DOM node.What This Means
When you write:Benefits
- Predictable - You know exactly what gets rendered
- Inspectable - DevTools show the real DOM structure
- Styleable - Direct access to elements for CSS
- Debuggable - No hidden wrapper elements to confuse you
Exceptions Are Documented
When a component deviates from this pattern (likeDialog.Portal which renders to a different part of the DOM), it’s clearly documented with rationale.
Ref Forwarding
Refs are forwarded to the underlying DOM element, working exactly as you’d expect with native elements.Basic Ref Usage
triggerRef.currentis the<button>elementcontentRef.currentis the<div>element containing the dialog content
Accessing DOM Methods
Because refs point to real DOM nodes, you can use any DOM API:Ref Composition
Radix uses theuseComposedRefs hook internally to merge multiple refs. This is from packages/react/compose-refs/src/compose-refs.tsx:55:
The asChild Prop
The asChild prop is the key to advanced composition. It tells Radix to merge its functionality with your own element instead of rendering a default one.
Without asChild
With asChild
Radix merges its props (event handlers, aria attributes, etc.) with your element’s props. Your element’s props take precedence where appropriate.
How It Works
TheasChild prop leverages Radix’s Slot component (from packages/react/slot/src/slot.tsx:45):
- Takes the child element you provide
- Clones it with Radix’s props merged in
- Forwards refs correctly
Event Handler Composition
Event handlers compose automatically, allowing you to add custom logic without breaking built-in behavior.Adding Your Own Handlers
Handler Execution Order
- Your handler runs first
- Radix’s internal handler runs second
- Event propagates unless stopped
Preventing Default Behavior
You can prevent Radix’s behavior when needed:How It Works
Radix usescomposeEventHandlers internally (from @radix-ui/primitive):
- Your handler runs first
- Radix respects
preventDefault() - Event propagation works naturally
Composition Patterns
Pattern 1: Wrapping with Custom Components
Create your own components that wrap Radix:Pattern 2: Polymorphic Components
Create components that can render as different elements:Pattern 3: Animation Libraries
Compose with animation libraries like Framer Motion:Pattern 4: Extending with Additional Props
Add your own props while preserving Radix functionality:Prop Merging Behavior
When usingasChild, props are merged intelligently:
Event Handlers
Both handlers run (yours first, then Radix’s):myHandler and Radix’s internal handler execute.
Styles
Style objects are merged:Class Names
Class names are concatenated:className="my-button [radix-classes]".
Other Props
Child props take precedence:packages/react/slot/src/slot.tsx:164:
Real-World Examples
Example 1: Custom Accordion Trigger
Example 2: Dialog with Custom Close Button
Example 3: Checkbox with Label Composition
Summary
Radix UI’s composability gives you:- Direct DOM access through 1-to-1 component mapping
- Predictable refs that point to actual DOM elements
- Flexible composition via the
asChildprop - Natural event handling with automatic handler composition
- Full control over rendering and behavior
Related Concepts
- Philosophy - Understand the principles behind composability
- Customization - Learn how to style composed components
- State Management - Control component state in composed structures