useRender
Hook for enabling a render prop in custom components.
View as MarkdownThe useRender hook lets you build custom components that provide a render prop to override the default rendered element.
Examples
A render prop for a custom Text component lets consumers use it to replace the default rendered p element with a different tag or component.
Text component rendered as a paragraph tag
Text component rendered as a strong tagThe callback version of the render prop enables more control of how props are spread, and also passes the internal state of a component.
Merging props
The mergeProps function merges two or more sets of React props together. It safely merges three types of props:
- Event handlers, so that all are invoked
classNamestringsstyleproperties
mergeProps merges objects from left to right, so that subsequent objects’ properties in the arguments overwrite previous ones. Merging props is useful when creating custom components, as well as inside the callback version of the render prop for any Base UI component.
import { mergeProps } from '@base-ui/react/merge-props';
import styles from './index.module.css';
function Button() {
return (
<Component
render={(props, state) => (
<button
{...mergeProps<'button'>(props, {
className: styles.Button,
})}
/>
)}
/>
);
}Merging refs
When building custom components, you often need to control a ref internally while still letting external consumers pass their own—merging refs lets both parties have access to the underlying DOM element. The ref option in useRender enables this, which holds an array of refs to be merged together.
In React 19, React.forwardRef() is not needed when building primitive components, as the external ref prop is already contained inside props. Your internal ref can be passed to ref to be merged with props.ref:
function Text({ render, ...props }: TextProps) {
const internalRef = React.useRef<HTMLElement | null>(null);
const element = useRender({
defaultTagName: 'p',
ref: internalRef,
props,
render,
});
return element;
}In older versions of React, you need to use React.forwardRef() and add the forwarded ref to the ref array along with your own internal ref.
The examples above assume React 19, and should be modified to use React.forwardRef() to support React 18 and 17.
const Text = React.forwardRef(function Text(
{ render, ...props }: TextProps,
forwardedRef: React.ForwardedRef<HTMLElement>,
) {
const internalRef = React.useRef<HTMLElement | null>(null);
const element = useRender({
defaultTagName: 'p',
ref: [forwardedRef, internalRef],
props,
render,
});
return element;
});TypeScript
To type props, there are two interfaces:
useRender.ComponentPropsfor a component’s external (public) props. It types therenderprop and HTML attributes.useRender.ElementPropsfor the element’s internal (private) props. It types HTML attributes alone.
interface ButtonProps extends useRender.ComponentProps<'button'> {}
function Button({ render, ...props }: ButtonProps) {
const defaultProps: useRender.ElementProps<'button'> = {
className: styles.Button,
type: 'button',
children: 'Click me',
};
const element = useRender({
defaultTagName: 'button',
render,
props: mergeProps<'button'>(defaultProps, props),
});
return element;
}Migrating from Radix UI
Radix UI uses an asChild prop, while Base UI uses a render prop. Learn more about how composition works in Base UI in the composition guide.
In Radix UI, the Slot component lets you implement an asChild prop.
import { Slot } from 'radix-ui';
function Button({ asChild, ...props }) {
const Comp = asChild ? Slot.Root : 'button';
return <Comp {...props} />;
}
// Usage
<Button asChild>
<MyButton className="primary">Submit</MyButton>
</Button>In Base UI, useRender lets you implement a render prop. The example below is the equivalent implementation to the Radix example above.
import { useRender } from '@base-ui/react/use-render';
function Button({ render, ...props }) {
return useRender({
defaultTagName: 'button',
render,
props,
});
}
// Usage
<Button render={<MyButton className="primary" />}>Submit</Button>Render prop and polymorphism
The render prop is primarily designed for composing event handlers and behavioral props. In most cases it should render the same tag as the default element.
Using render for polymorphism (rendering a different tag) requires more care, as some default props may not be valid on the new element. For example, type="button" is only valid on a <button>. Since the component can’t know what element render will produce at render time and before hydration, props like these need an explicit signal. This is why Base UI’s Button provides a nativeButton prop to control which defaults are applied.
API reference
const element = useRender({
// Input parameters
});Parameters
useRender.Parameters
renderReactElement | function
- Name
- Description
The React element or a function that returns one to override the default element.
- Type
UseRenderRenderProp<TState> | undefined
refUnion
- Name
- Description
The ref to apply to the rendered element.
- Type
| React.Ref<Element>[] | React.Ref<Element> | undefined
stateTState
- Name
- Description
The state of the component, passed as the second argument to the
rendercallback. State properties are automatically converted to data-* attributes.- Type
TState | undefined
stateAttributesMappingStateAttributesMapping<TState>
- Description
Custom mapping for converting state properties to data-* attributes.
- Type
StateAttributesMapping<TState> | undefined- Example
{ isActive: (value) => (value ? { 'data-is-active': '' } : null) }
propsRecord<string, unknown>
- Name
- Description
Props to be spread on the rendered element. They are merged with the internal props of the component, so that event handlers are merged,
classNamestrings andstyleproperties are joined, while other external props overwrite the internal ones.- Type
Record<string, unknown> | undefined
enabledboolean
- Name
- Description
If
false, the hook will skip most of its internal logic and returnnull. This is useful for rendering a component conditionally.- Type
boolean | undefined
defaultTagNameUnion
- Name
- Description
The default tag name to use for the rendered element when
renderis not provided.- Type
| 'symbol' | 'object' | 'a' | 'abbr' | 'address' | 'area' | 'article' | 'aside' | 'audio' | 'b' | 'base' | 'bdi' | 'bdo' | 'big' | 'blockquote' | 'body' | 'br' | 'button' | 'canvas' | 'caption' | 'center' | 'cite' | 'code' | 'col' | 'colgroup' | 'data' | 'datalist' | 'dd' | 'del' | 'details' | 'dfn' | 'dialog' | 'div' | 'dl' | 'dt' | 'em' | 'embed' | 'fieldset' | 'figcaption' | 'figure' | 'footer' | 'form' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'head' | 'header' | 'hgroup' | 'hr' | 'html' | 'i' | 'iframe' | 'img' | 'input' | 'ins' | 'kbd' | 'keygen' | 'label' | 'legend' | 'li' | 'link' | 'main' | 'map' | 'mark' | 'menu' | 'menuitem' | 'meta' | 'meter' | 'nav' | 'noindex' | 'noscript' | 'ol' | 'optgroup' | 'option' | 'output' | 'p' | 'param' | 'picture' | 'pre' | 'progress' | 'q' | 'rp' | 'rt' | 'ruby' | 's' | 'samp' | 'search' | 'slot' | 'script' | 'section' | 'select' | 'small' | 'source' | 'span' | 'strong' | 'style' | 'sub' | 'summary' | 'sup' | 'table' | 'template' | 'tbody' | 'td' | 'textarea' | 'tfoot' | 'th' | 'thead' | 'time' | 'title' | 'tr' | 'track' | 'u' | 'ul' | 'var' | 'video' | 'wbr' | 'webview' | 'svg' | 'animate' | 'animateMotion' | 'animateTransform' | 'circle' | 'clipPath' | 'defs' | 'desc' | 'ellipse' | 'feBlend' | 'feColorMatrix' | 'feComponentTransfer' | 'feComposite' | 'feConvolveMatrix' | 'feDiffuseLighting' | 'feDisplacementMap' | 'feDistantLight' | 'feDropShadow' | 'feFlood' | 'feFuncA' | 'feFuncB' | 'feFuncG' | 'feFuncR' | 'feGaussianBlur' | 'feImage' | 'feMerge' | 'feMergeNode' | 'feMorphology' | 'feOffset' | 'fePointLight' | 'feSpecularLighting' | 'feSpotLight' | 'feTile' | 'feTurbulence' | 'filter' | 'foreignObject' | 'g' | 'image' | 'line' | 'linearGradient' | 'marker' | 'mask' | 'metadata' | 'mpath' | 'path' | 'pattern' | 'polygon' | 'polyline' | 'radialGradient' | 'rect' | 'set' | 'stop' | 'switch' | 'text' | 'textPath' | 'tspan' | 'use' | 'view' | undefined
Return value
ReactElement | nulluseRender.StateHide
type useRenderState = {}useRender.ComponentPropsHide
type useRenderComponentProps<
ElementType extends React.ElementType,
TState = {},
RenderFunctionProps = HTMLProps,
> = React.ComponentPropsWithRef<ElementType> & {
render?: ReactElement | ((props: RenderFunctionProps, state: TState) => ReactElement);
}useRender.ElementPropsHide
type useRenderElementProps =
| (React.PropsWithoutRef<Props> & React.RefAttributes<R | any>)
| Props
| React.ComponentProps<React.ElementType>useRender.ParametersHide
type useRenderParameters<
TState,
RenderedElementType extends Element,
Enabled extends boolean | undefined,
> = {
/** The React element or a function that returns one to override the default element. */
render?: UseRenderRenderProp<TState>;
/** The ref to apply to the rendered element. */
ref?: React.Ref<RenderedElementType>[] | React.Ref<RenderedElementType>;
/**
* The state of the component, passed as the second argument to the `render` callback.
* State properties are automatically converted to data-* attributes.
*/
state?: TState;
/**
* Custom mapping for converting state properties to data-* attributes.
* @example { isActive: (value) => (value ? { 'data-is-active': '' } : null) }
*/
stateAttributesMapping?: StateAttributesMapping<TState>;
/**
* Props to be spread on the rendered element.
* They are merged with the internal props of the component, so that event handlers
* are merged, `className` strings and `style` properties are joined, while other external props overwrite the
* internal ones.
*/
props?: Record<string, unknown>;
/**
* If `false`, the hook will skip most of its internal logic and return `null`.
* This is useful for rendering a component conditionally.
* @default true
*/
enabled?: boolean | undefined;
/**
* The default tag name to use for the rendered element when `render` is not provided.
* @default 'div'
*/
defaultTagName?:
| 'symbol'
| 'object'
| 'a'
| 'abbr'
| 'address'
| 'area'
| 'article'
| 'aside'
| 'audio'
| 'b'
| 'base'
| 'bdi'
| 'bdo'
| 'big'
| 'blockquote'
| 'body'
| 'br'
| 'button'
| 'canvas'
| 'caption'
| 'center'
| 'cite'
| 'code'
| 'col'
| 'colgroup'
| 'data'
| 'datalist'
| 'dd'
| 'del'
| 'details'
| 'dfn'
| 'dialog'
| 'div'
| 'dl'
| 'dt'
| 'em'
| 'embed'
| 'fieldset'
| 'figcaption'
| 'figure'
| 'footer'
| 'form'
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'head'
| 'header'
| 'hgroup'
| 'hr'
| 'html'
| 'i'
| 'iframe'
| 'img'
| 'input'
| 'ins'
| 'kbd'
| 'keygen'
| 'label'
| 'legend'
| 'li'
| 'link'
| 'main'
| 'map'
| 'mark'
| 'menu'
| 'menuitem'
| 'meta'
| 'meter'
| 'nav'
| 'noindex'
| 'noscript'
| 'ol'
| 'optgroup'
| 'option'
| 'output'
| 'p'
| 'param'
| 'picture'
| 'pre'
| 'progress'
| 'q'
| 'rp'
| 'rt'
| 'ruby'
| 's'
| 'samp'
| 'search'
| 'slot'
| 'script'
| 'section'
| 'select'
| 'small'
| 'source'
| 'span'
| 'strong'
| 'style'
| 'sub'
| 'summary'
| 'sup'
| 'table'
| 'template'
| 'tbody'
| 'td'
| 'textarea'
| 'tfoot'
| 'th'
| 'thead'
| 'time'
| 'title'
| 'tr'
| 'track'
| 'u'
| 'ul'
| 'var'
| 'video'
| 'wbr'
| 'webview'
| 'svg'
| 'animate'
| 'animateMotion'
| 'animateTransform'
| 'circle'
| 'clipPath'
| 'defs'
| 'desc'
| 'ellipse'
| 'feBlend'
| 'feColorMatrix'
| 'feComponentTransfer'
| 'feComposite'
| 'feConvolveMatrix'
| 'feDiffuseLighting'
| 'feDisplacementMap'
| 'feDistantLight'
| 'feDropShadow'
| 'feFlood'
| 'feFuncA'
| 'feFuncB'
| 'feFuncG'
| 'feFuncR'
| 'feGaussianBlur'
| 'feImage'
| 'feMerge'
| 'feMergeNode'
| 'feMorphology'
| 'feOffset'
| 'fePointLight'
| 'feSpecularLighting'
| 'feSpotLight'
| 'feTile'
| 'feTurbulence'
| 'filter'
| 'foreignObject'
| 'g'
| 'image'
| 'line'
| 'linearGradient'
| 'marker'
| 'mask'
| 'metadata'
| 'mpath'
| 'path'
| 'pattern'
| 'polygon'
| 'polyline'
| 'radialGradient'
| 'rect'
| 'set'
| 'stop'
| 'switch'
| 'text'
| 'textPath'
| 'tspan'
| 'use'
| 'view';
}useRender.RenderPropHide
type useRenderRenderProp<TState = Record<string, unknown>> =
| ReactElement
| ((props: React.HTMLAttributes<any>, state: TState) => ReactElement)useRender.ReturnValueHide
type useRenderReturnValue = ReactElement | nullUseRenderComponentPropsHide
type UseRenderComponentProps<
ElementType extends React.ElementType,
State = {},
RenderFunctionProps = HTMLProps,
> = React.ComponentPropsWithRef<ElementType> & {
render?: ReactElement | ((props: RenderFunctionProps, state: State) => ReactElement);
}UseRenderElementPropsHide
type UseRenderElementProps =
| (React.PropsWithoutRef<Props> & React.RefAttributes<R | any>)
| Props
| React.ComponentProps<React.ElementType>UseRenderParametersHide
type UseRenderParameters<
State,
RenderedElementType extends Element,
Enabled extends boolean | undefined,
> = {
/** The React element or a function that returns one to override the default element. */
render?: UseRenderRenderProp<State>;
/** The ref to apply to the rendered element. */
ref?: React.Ref<RenderedElementType> | React.Ref<RenderedElementType>[];
/**
* The state of the component, passed as the second argument to the `render` callback.
* State properties are automatically converted to data-* attributes.
*/
state?: State;
/**
* Custom mapping for converting state properties to data-* attributes.
* @example { isActive: (value) => (value ? { 'data-is-active': '' } : null) }
*/
stateAttributesMapping?: StateAttributesMapping<State>;
/**
* Props to be spread on the rendered element.
* They are merged with the internal props of the component, so that event handlers
* are merged, `className` strings and `style` properties are joined, while other external props overwrite the
* internal ones.
*/
props?: Record<string, unknown>;
/**
* If `false`, the hook will skip most of its internal logic and return `null`.
* This is useful for rendering a component conditionally.
* @default true
*/
enabled?: boolean | undefined;
/**
* The default tag name to use for the rendered element when `render` is not provided.
* @default 'div'
*/
defaultTagName?:
| 'symbol'
| 'object'
| 'a'
| 'abbr'
| 'address'
| 'area'
| 'article'
| 'aside'
| 'audio'
| 'b'
| 'base'
| 'bdi'
| 'bdo'
| 'big'
| 'blockquote'
| 'body'
| 'br'
| 'button'
| 'canvas'
| 'caption'
| 'center'
| 'cite'
| 'code'
| 'col'
| 'colgroup'
| 'data'
| 'datalist'
| 'dd'
| 'del'
| 'details'
| 'dfn'
| 'dialog'
| 'div'
| 'dl'
| 'dt'
| 'em'
| 'embed'
| 'fieldset'
| 'figcaption'
| 'figure'
| 'footer'
| 'form'
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'head'
| 'header'
| 'hgroup'
| 'hr'
| 'html'
| 'i'
| 'iframe'
| 'img'
| 'input'
| 'ins'
| 'kbd'
| 'keygen'
| 'label'
| 'legend'
| 'li'
| 'link'
| 'main'
| 'map'
| 'mark'
| 'menu'
| 'menuitem'
| 'meta'
| 'meter'
| 'nav'
| 'noindex'
| 'noscript'
| 'ol'
| 'optgroup'
| 'option'
| 'output'
| 'p'
| 'param'
| 'picture'
| 'pre'
| 'progress'
| 'q'
| 'rp'
| 'rt'
| 'ruby'
| 's'
| 'samp'
| 'search'
| 'slot'
| 'script'
| 'section'
| 'select'
| 'small'
| 'source'
| 'span'
| 'strong'
| 'style'
| 'sub'
| 'summary'
| 'sup'
| 'table'
| 'template'
| 'tbody'
| 'td'
| 'textarea'
| 'tfoot'
| 'th'
| 'thead'
| 'time'
| 'title'
| 'tr'
| 'track'
| 'u'
| 'ul'
| 'var'
| 'video'
| 'wbr'
| 'webview'
| 'svg'
| 'animate'
| 'animateMotion'
| 'animateTransform'
| 'circle'
| 'clipPath'
| 'defs'
| 'desc'
| 'ellipse'
| 'feBlend'
| 'feColorMatrix'
| 'feComponentTransfer'
| 'feComposite'
| 'feConvolveMatrix'
| 'feDiffuseLighting'
| 'feDisplacementMap'
| 'feDistantLight'
| 'feDropShadow'
| 'feFlood'
| 'feFuncA'
| 'feFuncB'
| 'feFuncG'
| 'feFuncR'
| 'feGaussianBlur'
| 'feImage'
| 'feMerge'
| 'feMergeNode'
| 'feMorphology'
| 'feOffset'
| 'fePointLight'
| 'feSpecularLighting'
| 'feSpotLight'
| 'feTile'
| 'feTurbulence'
| 'filter'
| 'foreignObject'
| 'g'
| 'image'
| 'line'
| 'linearGradient'
| 'marker'
| 'mask'
| 'metadata'
| 'mpath'
| 'path'
| 'pattern'
| 'polygon'
| 'polyline'
| 'radialGradient'
| 'rect'
| 'set'
| 'stop'
| 'switch'
| 'text'
| 'textPath'
| 'tspan'
| 'use'
| 'view';
}UseRenderRenderPropHide
type UseRenderRenderProp<State = Record<string, unknown>> =
| ReactElement
| ((props: React.HTMLAttributes<any>, state: State) => ReactElement)UseRenderReturnValueHide
type UseRenderReturnValue = ReactElement | nullUseRenderStateHide
type UseRenderState = {}