shadcn registry

A drop-in replacement for the shadcn/ui components that bedrock can actually back. Same exports, same data-slot attributes, same class names — the diff in your app is the import inside components/ui/*.tsx, and nothing above it.

npx shadcn@latest add https://bedrock.sams.land/r/dialog.json
npx shadcn@latest add https://bedrock.sams.land/r/dropdown-menu.json

Each one overwrites the matching file in components/ui/. Commit first; the CLI asks before overwriting, and the diff is the thing worth reading.

Coverage

24 registry items, covering every shadcn/ui component that was backed by a Radix primitive.

shadcn component status notes
dialog replaced Uncontrolled. Portal and Overlay are kept as no-ops so blocks keep compiling.
dialog-controlled added Same file, controlled entry point, required open. Not a shadcn name — install it as dialog when you need the veto.
alert-dialog replaced role="alertdialog" on the same primitive. Action/Cancel are close buttons.
popover, tooltip, hover-card replaced Anchor positioning; side/align/sideOffset keep their names.
dropdown-menu, context-menu replaced Popover plus roving focus. ContextMenuTrigger renders its children in place — the root is the trigger area.
select replaced A real <select>. SelectValue is <selectedcontent>; there is no placeholder render prop, because the closed state mirrors the chosen option's markup.
checkbox, switch, radio-group, slider, progress replaced Native inputs. Every Indicator and Thumb element is gone; the marks are pseudo-elements, which is a smaller component and a bigger CSS change.
accordion, collapsible, tabs replaced <details> for the first two. AccordionTrigger renders inside the <summary>.
label, separator, aspect-ratio, avatar, scroll-area, toggle, toggle-group replaced
menubar, navigation-menu, toast/sonner, sheet, drawer pending Primitives exist; the wrapper is not written. sheet and drawer are Dialog plus positioning.
combobox, command stays on Radix Built on cmdk, not on a Radix primitive.
button, card, input, table, badge, and the other unstyled-div components unaffected They never used Radix primitives.

Mixing is safe: Radix and bedrock are separate packages with no shared globals and no CSS collisions. It does mean both are in your bundle until the migration finishes — see gaps.

What changed inside the component

DialogPortal renders its children in place

<dialog> is promoted to the top layer by showModal(), so it paints above everything regardless of where it sits in the tree. There is nothing to portal past. The export is kept because shadcn blocks import it.

Consequence: the container prop is accepted and ignored. If you were portalling into a specific subtree to inherit a theme context, that now happens naturally, because the element never moves.

DialogOverlay renders nothing

The overlay is ::backdrop, a pseudo-element. It cannot be a React node, so classes passed to DialogOverlay have nowhere to go — in development you get one console warning pointing at the backdrop: variant on DialogContent, where those styles now live.

If your overlay had children (a spinner, a close affordance), that is a redesign rather than a migration. See gaps.

Animation keys off :open

-data-[state=open]:animate-in data-[state=closed]:animate-out
+open:opacity-100 open:scale-100 starting:open:opacity-0 transition-discrete

starting: is @starting-style and transition-discrete is transition-behavior: allow-discrete — both are built into Tailwind 4. There is no forceMount and no presence wrapper; the element is always in the DOM and the browser sequences the exit.

Tailwind 3 needs arbitrary variants ([&:open]:, [&::backdrop]:) and has no starting:, so entry transitions need a hand-written @starting-style block.

Dialog has no open prop in the default item

That is the two-root split reaching user land. dialog.tsx imports @apostel/bedrock, which has no reconciliation code in it at all; passing open throws in development with the command to install the controlled build instead.

onOpenChange still exists on the uncontrolled build and still fires — it just cannot refuse. Most shadcn usage (onOpenChange={() => form.reset()}) is served by it.

AlertDialogAction and AlertDialogCancel

Both render Dialog.Close, so both dismiss. Put the destructive work in onClick; you do not need to close it yourself.

<AlertDialogAction onClick={() => deleteProject(id)}>Delete</AlertDialogAction>

Radix's Action behaves the same way, so this is a like-for-like swap.

Building the registry

npm run registry:build

Reads registry.json, inlines each component's source, and writes r/*.json — the same output shape shadcn build produces, generated so a component and its registry entry cannot drift.

r/ is published as part of the docs site, so the registry URL and the documentation are the same host.