The render prop
Every component takes render. It replaces the component’s default element with whatever you
pass, merging the component’s props, classes, handlers and ref onto it.
<Chip render={<li />}>React</Chip><Button render={<motion.button />}>Save</Button><Card render={<article />} lift>…</Card>Why not as
Section titled “Why not as”Chip used to take as?: 'span' | 'button'. A closed union can only ever list the elements
its author happened to think of, and the moment a consumer needs something else the component
is useless to them. In this project’s own consumer that happened three times — the class names
were copied out by hand, and the component was abandoned at those call sites.
render has no such ceiling: any element, any component, including ones that do not exist yet.
How merging works
Section titled “How merging works”| Prop | Behaviour |
|---|---|
| event handlers | chained, most specific first — the render element’s, then yours, then the component’s |
className | concatenated — both apply |
style | shallow-merged |
ref | merged — both the component and your ref receive the node |
| everything else | later wins, so you can override aria-label, id, tabIndex |
undefined values are skipped, so an absent prop never clobbers one the component set.
Because handlers run before the component’s own, you can stop it from acting:
<Switch.Root onClick={(event) => event.preventComponentHandler()} />preventDefault() is left alone — it still means only “cancel the browser’s default action”.
The element owns its own attributes
Section titled “The element owns its own attributes”State belongs on the component; the render element only decides what gets rendered. The
component derives attributes such as disabled, aria-* and data-* from its own props, and
the render element’s own props win the merge — so writing one there overrides the markup, never
the component’s state.
<Switch.Root disabled render={<motion.button />} /> // correct<Switch.Root render={<motion.button disabled />} /> // markup only — `data-disabled` and the form input stay enabledNothing is checked or warned about: what you pass through render is yours.
The same applies to type on a button: <Chip render={<button type="button" />}>, or it
defaults to type="submit".
Accessibility is the point
Section titled “Accessibility is the point”render exists so styling never costs you semantics. A chip inside a list stays an li; a
clickable chip stays a focusable button; a card wrapping navigation stays a labelled nav.
If you find yourself copying class names to get the right element, that is the bug render
was added to fix.