Implement the render prop pattern with Base UI's useRender hook for polymorphic components. Consult this skill whenever changing which HTML element or React component a component renders as, composing Base UI primitives, adding router Link integration to buttons, migrating from asChild to render prop, or implementing element composition.
Install
npx skillscat add kvnwolf/devtools/polymorphic-components Install via the SkillsCat registry.
SKILL.md
Polymorphic Components
Guidelines for implementing the render prop pattern using Base UI's useRender hook. This pattern allows consumers to change the underlying HTML element or React component that a component renders.
Core Concept
The render prop enables semantic flexibility without breaking component behavior:
// Renders as a div (default)
<Component.Title>Page Title</Component.Title>
// Renders as an h1
<Component.Title render={<h1 />}>Page Title</Component.Title>
// Renders as a link
<Button nativeButton={false} render={<a href="/about" />}>About Us</Button>When to Use
- Semantic HTML: Render a title as the appropriate heading level (
h1-h6) - Navigation: Render buttons as links or router components
- Composition: Compose multiple Base UI components together
- Accessibility: Use the correct element for the context
Implementation
import { useRender } from "@base-ui/react/use-render";
import { mergeProps } from "@base-ui/react/merge-props";
import { cn } from "@/lib/utils";
export function Title({
render,
className,
...props
}: useRender.ComponentProps<"div">) {
return useRender({
render,
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn("font-medium tracking-tight", className),
},
props
),
});
}useRender Parameters
| Parameter | Type | Description |
|---|---|---|
render |
ReactElement | undefined |
Element to render instead of default |
defaultTagName |
keyof JSX.IntrinsicElements |
HTML tag when render is not provided |
props |
object |
Props to pass to the rendered element |
Usage Patterns
Change HTML Element
<Component.Title render={<h1 />}>
Component Title
</Component.Title>Render as Link
<Button nativeButton={false} render={<a href="/about" />}>
About Us
</Button>Router Integration
import { Link } from "@tanstack/react-router";
<Button nativeButton={false} render={<Link to="/dashboard" />}>
Dashboard
</Button>Custom Components
<Card.Title render={<MyHeading level={2} />}>
Card Title
</Card.Title>Nested Composition
<Dialog.Trigger
render={
<Tooltip.Trigger render={<Button variant="outline" />} />
}
>
Open Dialog
</Dialog.Trigger>Known Issue: Custom Data Attributes
mergeProps has strict typing that doesn't recognize data-* attributes.
Error:
Object literal may only specify known properties, and '"data-slot"' does
not exist in type 'WithBaseUIEvent<DetailedHTMLProps<...>>'Workaround:
props: mergeProps<"div">(
{
className: cn("font-medium text-lg", className),
["data-slot" as string]: "empty-title",
},
props
),Quick Reference
| Task | Pattern |
|---|---|
| Add render prop | useRender.ComponentProps<"tagname"> as prop type |
| Merge props | mergeProps<"tagname">({ ...defaults }, props) |
| Data attributes | ["data-attr" as string]: "value" |
| Router links | render={<Link to="/path" />} |
| Native anchor | render={<a href="/path" />} |