Skip to content
Dice UI

Composition

Docs

How Dice UI components compose with your own elements, components, props and refs.

Dice UI's Base UI components follow Base UI's composition model. Every part renders a default element, and the render prop lets you swap it out while keeping the part's behavior, props and ref.

The render prop

Pass an element to render to replace the default element. Dice UI merges its own props, such as ARIA attributes, data attributes, event handlers and the ref, into your element.

import { Timeline, TimelineItem } from "@/components/ui/timeline";
 
return (
  <Timeline render={<ol />}>
    <TimelineItem render={<li />}>Design review</TimelineItem>
  </Timeline>
)

Pass a function instead when you need control over where the props go, or want to read the part's state.

<TimelineItem
  render={(props) => (
    <li {...props}>
      <a href="/changelog">Release notes</a>
    </li>
  )}
/>

Combining components

render accepts any component that spreads its props onto a DOM element and accepts a ref. That includes other Base UI and shadcn/ui parts, so you can layer behaviors onto one element:

import { Archive } from "lucide-react";
 
import { ActionBarItem } from "@/components/ui/action-bar";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
 
return (
  <Tooltip>
    <TooltipTrigger render={<ActionBarItem />}>
      <Archive />
    </TooltipTrigger>
    <TooltipContent>Archive</TooltipContent>
  </Tooltip>
)

The rendered button is both the tooltip trigger and the action bar item.

Building your own parts

To give your own components a render prop, use the same tools Dice UI uses: useRender and mergeProps from @base-ui/react, and useMergedRefs from @base-ui/utils.

pnpm add @base-ui/react @base-ui/utils
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { useMergedRefs } from "@base-ui/utils/useMergedRefs";
import * as React from "react";
 
interface CardProps
  extends React.ComponentProps<"div">,
    useRender.ComponentProps<"div"> {}
 
function Card({ render, ref, ...props }: CardProps) {
  const localRef = React.useRef<HTMLDivElement>(null);
  const composedRef = useMergedRefs(ref, localRef);
 
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      { "data-slot": "card", ref: composedRef },
      props,
    ),
  });
}
  • useRender renders defaultTagName, or the element or function passed to render, with the merged props.
  • mergeProps merges props left to right. Event handlers are chained, className and style are combined, and other props are overridden. It doesn't merge refs.
  • useMergedRefs combines several refs (callback refs or ref objects) into one stable callback ref. Use it when your part needs its own ref as well as the one passed in.

Migrating from compose-refs

Dice UI used to ship a compose-refs utility (@diceui/compose-refs). It's deprecated, and components no longer install it. Swap the hook for useMergedRefs:

import { useComposedRefs } from "@/lib/compose-refs"; 
import { useMergedRefs } from "@base-ui/utils/useMergedRefs"; 

Then delete lib/compose-refs.ts. See the changelog for details.

On this page