Composition
How Dice UI components compose with your own elements, components, props and refs.
Dice UI's Radix components follow Radix's composition model. Every part renders a default element, and the asChild prop lets you render your own child instead while keeping the part's behavior, props and ref.
The asChild prop
With asChild, the part renders its only child instead of its default element. Dice UI merges its own props, such as ARIA attributes, data attributes, event handlers and the ref, into that child.
import { Timeline, TimelineItem } from "@/components/ui/timeline";
return (
<Timeline asChild>
<ol>
<TimelineItem asChild>
<li>Design review</li>
</TimelineItem>
</ol>
</Timeline>
)Combining components
The child can be any component that spreads its props onto a DOM element and accepts a ref. That includes other Radix 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 asChild>
<ActionBarItem>
<Archive />
</ActionBarItem>
</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 an asChild prop, use the same tools Dice UI uses. All of them come from radix-ui:
Slotrenders the child, merging the slot's props into it.useComposedRefsandcomposeRefsfromradix-ui/internalcombine several refs.composeEventHandlersfromradix-ui/internalchains handlers and skips the internal one when yours callsevent.preventDefault().
pnpm add radix-uiimport { Slot as SlotPrimitive } from "radix-ui";
import { composeEventHandlers, useComposedRefs } from "radix-ui/internal";
import * as React from "react";
interface CardProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
function Card({ asChild, ref, onKeyDown, ...props }: CardProps) {
const localRef = React.useRef<HTMLDivElement>(null);
const composedRef = useComposedRefs(ref, localRef);
const CardPrimitive = asChild ? SlotPrimitive.Slot : "div";
return (
<CardPrimitive
data-slot="card"
{...props}
ref={composedRef}
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
if (event.key === "Escape") localRef.current?.blur();
})}
/>
);
}useComposedRefs returns a stable callback ref, so use it during render. composeRefs is the plain function, for when you can't call a hook. Both accept callback refs and ref objects.
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. Import the same helpers from radix-ui instead:
import { useComposedRefs } from "@/lib/compose-refs";
import { useComposedRefs } from "radix-ui/internal"; Then delete lib/compose-refs.ts. See the changelog for details.