Composition
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/utilsimport { 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,
),
});
}useRenderrendersdefaultTagName, or the element or function passed torender, with the merged props.mergePropsmerges props left to right. Event handlers are chained,classNameandstyleare combined, and other props are overridden. It doesn't merge refs.useMergedRefscombines 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.