Composition
How prfct components fit together — parts, the render prop, data attributes for styling, and the rules that keep customizations maintainable.
prfct components are small, composable parts rather than configurable monoliths. A dialog is a Dialog, a DialogTrigger, a DialogContent, a DialogHeader — each one a thin, styled wrapper you can read in a minute.
Parts, not props
<Card>
<CardHeader>
<CardTitle>Team members</CardTitle>
<CardDescription>Invite people to collaborate.</CardDescription>
<CardAction>
<Button size="sm">Invite</Button>
</CardAction>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>…</CardFooter>
</Card>Parts keep APIs small and let you rearrange, omit or wrap any piece without the component growing a prop for every case.
The render prop
Base UI components render a sensible default element. To render something else — your own component, a router link — pass it to render. Behavior, accessibility and styles are merged onto it.
<DialogTrigger render={<Button variant="outline" />}>Edit profile</DialogTrigger>
<BreadcrumbLink render={<Link href="/projects" />}>Projects</BreadcrumbLink>asChild; Base UI uses render. When a button primitive renders a non-button element, set nativeButton={false} and Base UI adds role="button" and keyboard activation to it.Links that look like buttons
Navigation belongs on a link, even when it looks like a button. Don't render a Button as an anchor — Base UI would announce it as a button. Put buttonVariants on the link instead:
import { buttonVariants } from "@/components/ui/button"
<Link href="/pricing" className={buttonVariants({ variant: "outline" })}>
See pricing
</Link>Styling with data attributes
Every part carries a data-slot attribute, and interactive parts expose their state as data attributes (data-open, data-checked, data-disabled, data-highlighted…). Style against them from a parent without touching the component:
<Field className="*:data-[slot=field-description]:text-xs">…</Field>
<SelectItem className="data-highlighted:bg-brand-3">…</SelectItem>Customizing
Prefer these approaches, in order:
- Variants and sizes.
variant="outline",size="sm". - Layout with
className. Width, margin, alignment — layout belongs to the parent. - A new variant. If you need a new look, add it to the component's
cvadefinition so it's shared, documented and consistent. - Edit the source. It's your code. Keep tokens, focus styles and
data-slotattributes intact.
Merging classes
cn() merges class names and resolves Tailwind conflicts, so a className passed to a component overrides its defaults predictably. prfct's cn() knows the system's custom tokens — text-heading-md, shadow-floating, duration-fast — which a generic merger would mis-classify and silently drop.
import { cn } from "@/lib/utils"
cn("text-heading-md text-foreground", "text-muted-foreground")
// → "text-heading-md text-muted-foreground"