kvnwolf

polymorphic-components

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.

kvnwolf 4 Updated 3mo ago
GitHub

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" />}