Composition with Base UI (render)
ShadcnStore docs and blocks target Base UI (base-nova in components.json). Most components look and compose the same way; the patterns below are the ones that trip people up when wiring triggers, links, and selection controls.
The render prop
To render a component as another element (a Button, an anchor, a framework Link), pass that element to render. Put the visible children on the outer component — Base UI merges props into the render element and forwards children through.
<DialogTrigger render={<Button />}>Open</DialogTrigger>
<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
<Button render={<a href="/login" />} nativeButton={false}>
Login
</Button>
<SidebarMenuButton render={<a href="/settings" />}>Settings</SidebarMenuButton>
<Badge render={<a href="/changelog" />}>New</Badge>The trap: children inside render={<Button>Open</Button>} are overwritten and disappear. Always write render={<Button />} and put Open as children of the trigger.
Set nativeButton={false} when Button's render element is not a <button> (for example an <a> or Next.js Link).
The same render pattern applies to SheetTrigger, PopoverTrigger, TooltipTrigger, DropdownMenuTrigger, AlertDialogTrigger, CollapsibleTrigger, HoverCardTrigger, BreadcrumbLink, PaginationLink, NavigationMenuLink, and sidebar menu buttons.
Accordion
Base UI Accordion has no type or collapsible props. Selection mode is multiple?: boolean (default false). Values are always string arrays. Collapsing the open item is the default in single mode.
<Accordion defaultValue={["shipping"]}>
<AccordionItem value="shipping">…</AccordionItem>
</Accordion>
<Accordion multiple defaultValue={["item-1", "item-2"]}>
<AccordionItem value="item-1">…</AccordionItem>
<AccordionItem value="item-2">…</AccordionItem>
</Accordion>Toggle Group
No type prop. Use multiple for multi-select. value / defaultValue are always arrays.
<ToggleGroup defaultValue={["center"]}>
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup multiple defaultValue={["bold"]}>
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>Select — pass items for labels
Without items on the root, SelectValue shows the raw value ("apple") instead of the label ("Apple"). Prefer an array of { label, value } — it matches the official base-nova examples.
const fruits = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
{ label: "Grapes", value: "grapes" },
]
<Select items={fruits}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
{fruits.map((fruit) => (
<SelectItem key={fruit.value} value={fruit.value}>
{fruit.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>For positioning, SelectContent supports alignItemWithTrigger (defaults to true).
Data attributes for open state
Prefer Base UI attribute selectors in custom Tailwind:
| Prefer | Instead of |
|---|---|
data-open: | data-[state=open]: |
data-closed: | data-[state=closed]: |
data-active: | data-[state=active]: |
data-checked: | data-[state=checked]: |
data-pressed: | data-[state=on]: |
Sidebar's own group-data-[collapsible=icon]: utilities are unrelated — keep those.
Libraries that are not Base UI primitives
Some shadcn components wrap other libraries. Their composition APIs follow those libraries, not Base UI render:
- Drawer — built on Vaul; triggers follow Vaul's composition API, not Base UI
render - Command — built on cmdk
- Carousel — Embla · Chart — Recharts · Sonner — Sonner · Resizable — react-resizable-panels · Calendar — react-day-picker · Input OTP — input-otp
Related
- Installation: set up shadcn/ui with a
base-*style - components.json:
"style": "base-nova"for ShadcnStore blocks - Button, Dialog, Select, Accordion, Toggle Group: pages that lean on these patterns
FAQ
How do I compose a shadcn trigger with a Button?
Pass the Button as the render prop and put the label as children of the trigger:
<DialogTrigger render={<Button />}>Open</DialogTrigger>Children left inside the render element are overwritten.
How do I make a Button a link?
<Button render={<a href="/login" />} nativeButton={false}>
Login
</Button>Or apply buttonVariants() to the link's className.
Why does SelectValue show the raw value instead of the label?
Pass an items array on the Select root so Base UI can map values to labels.
What style should components.json use for ShadcnStore?
Use a base-* style such as base-nova. ShadcnStore docs and blocks target Base UI.