Skip to content

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.

tsx
<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.

tsx
<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.

tsx
<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.

tsx
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:

PreferInstead 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

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:

tsx
<DialogTrigger render={<Button />}>Open</DialogTrigger>

Children left inside the render element are overwritten.

tsx
<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.