One file, one source of truth

The previous post was about the problem. This one is the code — three real components that show how the dual-layer architecture plays out in practice.

Start with the exports

The first thing I check when I'm unfamiliar with a component is its export list. It tells you what the file thinks you might need.

Here's what switch.tsx exports in Lumi:

tsx
export {
  SwitchRoot,
  SwitchThumb,
  // Composite component
  Switch,
};

Compare that to a library that only exports the composite:

tsx
export { Switch }

Both get you a working switch. The difference shows up the moment you need something other than the default.

The Switch: same file, different component

The Switch composite is the 80% case. Drop it in, it works:

tsx
import { Switch } from "@/components/ui/switch"
 
<Switch defaultChecked />

But here's a custom switch — different shape, icon indicators, distinct checked/unchecked states:

tsx
import { SwitchRoot, SwitchThumb } from "@/components/ui/switch"
 
<SwitchRoot
  className={cn(
    "group relative inline-flex h-8 w-14 shrink-0 cursor-pointer items-center border border-transparent transition-colors duration-200 outline-none",
    "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
    "data-unchecked:bg-background data-unchecked:border-input data-unchecked:hover:bg-muted/70",
    "data-checked:bg-primary data-checked:border-primary data-checked:hover:bg-primary/90",
    className,
  )}
  data-slot="switch"
  {...props}
>
  <div
    aria-hidden="true"
    className="pointer-events-none absolute inset-0 flex size-full items-center"
  >
    <div className="flex h-full w-1/2 items-center justify-center">
      <MinusIcon className="size-4.5 text-primary-foreground rotate-90 opacity-0 transition-opacity duration-200 group-data-checked:opacity-100" />
    </div>
    <div className="flex h-full w-1/2 items-center justify-center">
      <CircleIcon className="size-3 text-muted-foreground opacity-100 transition-opacity duration-200 group-data-checked:opacity-0" />
    </div>
  </div>
 
  <SwitchThumb
    className={cn(
      "pointer-events-none relative block size-6 transition-all duration-200",
      "data-unchecked:translate-x-0.5 data-unchecked:bg-muted group-hover:data-unchecked:bg-primary/10",
      "data-checked:translate-x-6.5 data-checked:bg-background group-hover:data-checked:bg-background/90",
    )}
    data-slot="switch-thumb"
  />
</SwitchRoot>

Same import path. No new file. No drift.

With a library that only exports the composite, you'd either copy the file and modify it (two files that will diverge), or reach directly into @base-ui/react/switch, bypassing the library you just installed. Neither is wrong, but you're outside the system the moment you need something custom.

The Dialog: variants instead of copies

The Dialog file exports a composite called DialogContent. In most apps you'd use it for every dialog.

But what happens when a feature needs a bottom sheet on mobile, or a dialog that opens from the top of the screen? The typical answer is a second file: dialog-sheet.tsx. Lumi uses a layout prop instead:

tsx
<DialogContent layout="responsive">
  {/* centers on desktop, slides up from bottom on mobile */}
</DialogContent>
 
<DialogContent layout="top">
  {/* slides down from top */}
</DialogContent>
 
<DialogContent layout="scrollable">
  {/* fixed height, internal scroll */}
</DialogContent>
 
<DialogContent layout="stacked">
  {/* for nested dialogs — scales and offsets behind the foreground one */}
</DialogContent>

The variants are a cva definition in the same file. Adding a new layout means adding one case to the enum, typed, in the same place as everything else.

Positions
Behaviors

The // Composite component comment in the export block isn't just tidiness. Above it: building blocks. Below it: the assembled version. When you're scanning the file to figure out what to reach for, that comment is the landmark.

The Popover: the escape hatch

The data table in the playground has a date filter. Clicking a column header opens a dropdown with sorting options and two date modes: "Custom Date" and "Custom Range." Clicking either one opens a calendar that appears next to the menu item you clicked, while the data-heavy table behind it blurs into the background.

Here's a mini-demo:

Three things have to happen at once to make this work:

No trigger. The calendar has no PopoverTrigger — it's opened externally when a DropdownMenuItem is clicked. The dropdown stays alive (closeOnClick={false}) while the calendar is open.

Custom anchor. The calendar should appear next to the specific menu item that was clicked, not the original dropdown button. Each menu item holds a ref that gets passed to the calendar as its anchor.

Custom backdrop. The data table is visually dense. A blurring backdrop behind the calendar helps focus without closing the dropdown that's still visible.

tsx
// In the dropdown:
<DropdownMenuItem
  closeOnClick={false}
  onClick={() => setDateOpen(true)}
  ref={dateRef}           // this ref becomes the calendar's anchor
>
  Custom Date <CalendarIcon />
</DropdownMenuItem>
 
// The calendar, rendered outside the dropdown entirely:
<CalendarDatePicker
  open={dateOpen}
  onOpenChange={setDateOpen}
  ref={dateRef}           // passed in as anchor
  value={selectedDate}
  onValueChange={...}
/>

And the CalendarDatePicker itself — no composite in sight:

tsx
<Popover open={open} onOpenChange={onOpenChange}>
  <PopoverPortal>
    <PopoverBackdrop className="animate-fade bg-black/20 backdrop-blur-[2px] fixed inset-0" />
    <PopoverPositioner anchor={ref} side="left" sideOffset={12} disableAnchorTracking>
      <PopoverPopup className="relative w-80 rounded-md bg-popover animate-popup overlay-outline">
        <PopoverArrow />
        <Calendar ... />
      </PopoverPopup>
    </PopoverPositioner>
  </PopoverPortal>
</Popover>

PopoverContent assumes there's a trigger — it's built around that contract. This component has no trigger, a custom anchor from a different component tree, and a backdrop tailored to the data table context. The only way to build it is to assemble the pieces directly.

Same import path as always. The file just exports both layers.

A library that only exposes composites might export something like:

tsx
export {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverTitle,
  PopoverDescription,
}

PopoverPopup, PopoverPositioner, PopoverPortal, PopoverBackdrop — none of those available. For this use case you'd go directly to @base-ui/react/popover. You're not working within the library at that point, you're going around it.

When a composite gets a convenience prop

Here's a team-switcher where the left side is a link and only the chevron icon opens the popup:

tsx
const containerRef = useRef<HTMLDivElement | null>(null);
 
<div className="flex items-center justify-between gap-2 w-48" ref={containerRef}>
  <a href="#">
    <ComboboxValue />
  </a>
  <ComboboxTrigger
    render={
      <Button size="icon-sm" variant="ghost">
        <ChevronsUpDownIcon className="size-4" />
      </Button>
    }
  />
</div>
<ComboboxContent
  positionerAnchor={containerRef}
  side="bottom"
>
  ...
</ComboboxContent>

The trigger is a small icon button. By default, the popup anchors to the trigger — which means it would open at icon width. But the popup should match the container div (w-48), not the icon.

positionerAnchor={containerRef} points the positioner at the container instead. The popup opens at the right width, positioned under the full row.

ComboboxContent has this prop explicitly because it's a common enough need that users shouldn't have to know how the positioner works internally to solve it. You could reach for the primitives — ComboboxPositioner takes an anchor prop — but the convenience prop gets you there with autocomplete and no docs reading.

The architecture doesn't treat primitives as the always-correct answer. Sometimes a well-placed prop on the composite is the right call.

What you're actually looking at

Every component file in Lumi follows the same structure: thin primitive wrappers at the top (just data-slot and minimal defaults), cva variant definitions for anything that needs them, one or more composite components that assemble the primitives, and an export list with // Composite component marking the boundary.

Start with the composite. If you hit the edge of what it can do, the primitives are in the same file, with the same import path. Reaching for them doesn't feel like leaving the library.


In this series:

  1. Why I built Lumi UI — the problem and the idea behind it
  2. One file, one source of truth — the dual-layer architecture in practice
  3. Designing for AI agents — how the architecture becomes an instruction set
  4. The details that matter — animations, hit areas, and type safety