Should you switch?

Written to be used against the library. If you are evaluating rather than using, this is the page that matters; the rest of docs/ assumes you already decided.

The short version

All 29 primitives exist and Radix's own Dialog suite passes, so the arguments about coverage and correctness are largely settled. What is left is not:

The three that matter
Your component tests stop working The single biggest cost, and rarely what anyone checks first. jsdom implements none of the platform features this is built on.
The saving is uneven Where the platform hands over a whole element, almost nothing ships. Where it hands over only the layering, roving focus and typeahead still ship, and menus keep the largest bundle in the library.
Some things became CSS that used to be React Overlays, checkbox ticks, slider thumbs and scrollbars are pseudo-elements here. If your design puts content inside one, that is a redesign rather than a migration.

Everything below expands on those three, then lists what is missing or thin in what exists today.


Reasons not to migrate

Ten of them, worst first. Scan the table, then read the ones that apply to you.

# Reason Hits hardest if
1 Your component tests stop working You have a large jsdom suite and no Playwright
2 Chrome-first is not a formality You support Safari or Firefox today
3 The saving is uneven You are budgeting from the Select or Dialog headline
4 The trigger must be a <button> Your design system wraps buttons, or you use router links as triggers
5 data-state is gone You have shared CSS, mixins or Tailwind plugins keyed on it
6 Several parts render nothing Your design puts content inside an overlay, tick, thumb or scrollbar
7 No light dismiss by default Your users expect a backdrop click to close
8 No forceMount You animate presence with a library, or measure before opening
9 Interception hooks do not exist You rely on onInteractOutside and friends
10 It is young You need a deprecation policy and more than one contributor

1. Your component tests stop working

jsdom implements neither the top layer, nor invoker commands, nor anchor positioning. A React Testing Library test that clicks a trigger and asserts the dialog is visible will fail: in jsdom the click does nothing at all, because there is no handler behind it by design. There is no shim to install; the behaviour under test belongs to the parser.

// Passes against Radix. Fails against bedrock, in jsdom, forever.
render(<Dialog.Root>…</Dialog.Root>)
await user.click(screen.getByRole('button', { name: 'Delete' }))
expect(screen.getByRole('dialog')).toBeVisible()   // the click did nothing

Every dialog, menu, popover and tooltip test in your suite gets rewritten against a real browser. If you have hundreds of them and no Playwright setup, that cost dwarfs everything else on this page, and it lands before any of the benefits do.

2. Chrome-first is not a formality

The support matrix gives the minimum version of each engine per feature, and measures your own browser as you read it. Check it against the browsers you actually support before committing to anything. Invoker commands and anchor positioning are the two rows that decide whether you get "works" or "works and looks right".

interestfor, which would drive Tooltip and HoverCard, is not standardised and is in Chrome only. Those two ship on a JavaScript fallback as their normal path, not as a contingency.

3. The saving is uneven

group what replaces the JavaScript gzipped, Radix → bedrock
Select, Slider, Checkbox, Switch, RadioGroup, Progress, Separator, Label, AspectRatio, VisuallyHidden, ScrollArea the native element, effectively all of it Select 31.5 → 0.84 kB
Dialog, AlertDialog, Popover, Collapsible, Accordion top layer, invoker commands, <details name> Dialog 13.7 → 1.95 kB
Tooltip, HoverCard, Avatar, Toggle, Toast layering and dismissal, but not intent or image fallback Tooltip 19.3 → 2.55 kB
DropdownMenu, ContextMenu, Menubar, NavigationMenu, Tabs, Toolbar, ToggleGroup layering only, and roving.ts ships from either entry point DropdownMenu 31.6 → 3.55 kB

Budget from this table rather than from the Select headline, and read it by group rather than by size. Menus are the case people get backwards: they save more kilobytes than anything except Select, and they are also the largest thing bedrock ships, because roving focus and typeahead have to come from somewhere. /controlled buys almost nothing for that last group either.

4. The trigger must be a <button>, and it throws

Not a warning. A throw, at mount, in development.

The pattern that breaks most often is not a div. It is a router link:

<Dialog.Trigger asChild>
  <Link href="/upgrade">Upgrade</Link>   {/* renders <a>, so it throws */}
</Dialog.Trigger>

and the second most common is a <button> inside a <form> without type="button", which Radix silently made work and the platform silently ignores.

useDialogTrigger() is the escape hatch and hands you the accessibility problem along with the props. If your design system wraps every button in a component that renders a div, budget for that refactor before anything else.

5. data-state is gone, and :open cannot select an ancestor

[data-state="open"] → :open is a find-and-replace until the selector targeted a parent:

/* Radix */   .card:has([data-state="open"]) { … }
/* bedrock */ .card:has(dialog:open) { … }        /* :has() or nothing */

Shared utilities, design-system mixins and Tailwind plugins built around data-state all need rewriting, and the two conventions coexist badly while both libraries are installed.

6. Several parts render nothing, and that is a redesign not a rename

Each of these is an element in Radix and a pseudo-element or nothing here:

They keep their names so existing markup compiles, and they warn in development when handed a className. But an <input> cannot have children, so a <CheckIcon /> inside a checkbox has nowhere to go, and becomes a masked ::before instead. A spinner in an overlay, a scrollbar with its own hover animation, an icon component in a slider thumb: all redesigns.

7–10. The smaller ones

Real, but none of them is likely to decide the question on its own.

# Reason What it costs Workaround
7 No light dismiss by default A modal <dialog> does not close on a backdrop click. Users report this as "the dialog is stuck". closedby="any" restores it per dialog. There is no prop yet, because the rule against platform names in the public API rules out closedby and Radix has no name to copy.
8 No forceMount Closed content unmounts, as in Radix, but there is no opt-out for the cases that need the subtree alive while closed: an animation library driving presence, measuring before opening, keeping a video or iframe loaded. Hoist that state above the component.
9 Interception hooks do not exist onEscapeKeyDown, onPointerDownOutside, onInteractOutside, onOpenAutoFocus, onCloseAutoFocus, modal={false}, Portal container. Most have a platform answer: autofocus, cancel, closedby. "Most" is not "all", and the rest are unavailable rather than inconvenient.
10 It is young 0.1.1, one contributor, no deprecation policy. Several questions on this page have answers that would change public API. Adopting now means being the one who finds them.

Reasons it is still worth doing

Kept short, because the rest of the docs make this case.

If you are starting a new app, targeting evergreen Chrome, and already write Playwright tests, this is an easy call. If you have a mature Radix codebase with a jsdom suite, the honest answer is that the cost is front-loaded and lands mostly on your tests.

What is missing or thin in what exists today is a separate list: known gaps.