Skip to content

refactor(ui): move the overlay components from Radix to Base UI - #1123

Draft
EhabY wants to merge 2 commits into
mainfrom
ui-base-ui-migration
Draft

EhabY wants to merge 2 commits into
mainfrom
ui-base-ui-migration

Conversation

@EhabY

@EhabY EhabY commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Swaps the five @radix-ui/* packages in packages/ui for @base-ui/react. Styling and behavior are meant to be unchanged; the differences that could not be preserved are listed below.

Why now

Nothing outside packages/ui imports it yet, and the menus and Select appear only in stories and tests. This is the cheapest this swap will ever be.

What the switch bought

  • Five dependencies (61 @radix-ui packages in the store) become one with five transitive deps.
  • Base UI's context menu reuses the menu's own parts, so ContextMenu and DropdownMenu no longer build identical items from separate scopes. They keep only their root, trigger, and surface; everything else is shared as Menu*. The menuPart factory existed to bridge the two Radix scopes and is gone, replaced by plainly typed components.
  • --available-height, --anchor-width, and --transform-origin are named the same on every positioner, so the per-surface CSS mappings and the var(a, var(b)) fallback collapse.
  • The Select viewport scrollbar overrides existed only to undo Radix's scroll buttons, and are deleted.

Differences that could not be preserved

  • Select trigger labels. Radix rendered the selected item's ItemText. Base UI renders the raw value unless items declares the labels, and it only mounts items while open, so no wrapper can recover this. Callers now pass items (or a children function on SelectValue). placeholder is unchanged.
  • DropdownMenuLabel/ContextMenuLabel. Base UI's Menu.GroupLabel throws outside a Menu.Group. MenuLabel renders the same plain div Radix's Label did.
  • TooltipProvider. delayDuration is now delay, matching the underlying prop.
  • onValueChange/onOpenChange gain a second eventDetails argument.

Accessibility restored on top of Base UI

Two things Base UI omits that Radix provided, both covered by tests:

  • The tooltip popup had no role="tooltip" and no aria-describedby, leaving hovers out of the accessibility tree entirely. Both are wired back up, and the hooks that do it sit behind the hover-delegate early return so the tree's per-row tooltips do not pay for them.
  • A SelectItem with a description took its accessible name from its whole contents, so the muted second line was announced twice. Items are now labelled by their text.

Verification

pnpm typecheck, pnpm lint, pnpm test (2758 passed), pnpm build, and pnpm storybook:build all pass.

Visual parity still needs a Pixel run — that is the real check and I could not run it locally. To make a silent break less likely, test/webview/ui/overlays.test.tsx asserts that each surface still carries the classes, data-open, and positioner variables the stylesheets read.

Also in this branch

A second commit takes behavior assertions out of the packages/ui stories, leaving their play functions to interact and wait only. The two checks nothing else covered moved to unit tests.

Base UI serves the menu, context menu, select, and tooltip from one
package, and its context menu reuses the menu's own parts. ContextMenu
and DropdownMenu now share one set of items, keeping only their root,
trigger, and surface. The available-height, anchor-width, and
transform-origin variables are named the same on every positioner, so
the per-surface CSS mappings collapse.

Styling and behavior stay as they were, verified by measuring both
builds side by side: the select list, both menus, their submenus, and
every tooltip land on the pixels they did before. Where Base UI differs
it is corrected here rather than absorbed:

- Select.Icon passes a literal arrow as default children, which merged
  into the codicon instead of replacing it, so the trigger renders the
  icon directly.
- Base UI centers a popup on its anchor; the select list and context
  menu align to its start again, as Radix had them.
- A select row scrolled into view parked against the border because the
  popup scrolls itself and its padding is scrollable space, so
  scroll-padding restores the inset Radix got from a nested viewport.
- The tooltip is pinned to its own layer. Base UI only hints compositing
  above 1.5x, so below that its text picked up subpixel antialiasing and
  fringed against the hover background.
- Base UI leaves the tooltip out of the accessibility tree and names a
  select option from its whole contents, so the tooltip regains
  role="tooltip" and aria-describedby, and an option is labelled by its
  text rather than its description.

Stories in the package drive a component into the state Pixel
photographs, so their play functions only interact and wait; the checks
nothing else covered moved to unit tests.
@EhabY
EhabY force-pushed the ui-base-ui-migration branch from 49eaa91 to b60546d Compare September 22, 2026 15:06
Radix anchored its popups with a fixed strategy; Base UI defaults to
absolute. A fixed positioner is promoted to its own layer, and since
that layer is not opaque Chromium rasterizes the text inside it with
greyscale antialiasing. On absolute the tooltip picked up subpixel
antialiasing instead, fringing its glyphs orange and blue.

The select list scrolls a viewport inside the padded panel again, as it
did under Radix. The panel holds the padding and background while the
rows scroll within it, which puts the last row back against the list's
padding rather than the border, where the corner radius clipped it.

The stories reach their state declaratively rather than by tabbing to a
trigger, which needs the window focused, or arrowing into a submenu,
which left the first row's highlight up to when the screenshot landed.
Focusing an element directly keeps the focus ring and the highlight
without the timing, and the long list names its last pool so the
trigger keeps the truncation the story exists to show.

Every overlay story now renders identically run to run.
@EhabY
EhabY force-pushed the ui-base-ui-migration branch from 16b6bb0 to 06dc41e Compare September 22, 2026 16:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant