Move the OpenAPI package from react-aria to Base UI - #4473
Draft
nolannbiron wants to merge 15 commits into
Draft
Conversation
Three of react-aria's heaviest surfaces, and the ones the rest depend on. The select changes shape: react-aria cloned the selected item's children into the trigger, which Base UI's `Select.Value` cannot do, so `children` becomes a render function used for both the list and the trigger. It also owns its tooltip now — only it holds the trigger the tooltip anchors to. CSS follows the attributes Base UI actually emits: `data-entering`/`data-exiting` become `data-starting-style`/`data-ending-style`, `.react-aria-SelectValue` becomes `.openapi-select-value`, and `--trigger-width` becomes `--anchor-width`.
`Collapsible.Panel` unmounts when closed by default, matching the conditional render it replaces, so collapsed content still stays out of the DOM. The focus ring moves from a react-aria inline style to `:focus-visible`, and `[data-expanded="true"]` becomes `[data-open]`.
Base UI overlaps the trigger by default so the selected item lines up with it, like a native select. react-aria placed the popover below with an offset of 8, which is what the styling expects.
Base UI splits its animation attributes: `data-starting-style` and `data-ending-style` drive CSS transitions, while `data-open` and `data-closed` drive CSS animations. These styles use `@keyframes`, so the first mapping attached them to attributes that only live for one frame. The select popup also takes focus when it opens, which was drawing the browser's own focus ring on top of it.
The trigger had no outline reset, so the browser drew its own ring — Base UI returns focus to it when the popup closes, where react-aria moved focus into the list instead. The styled ring is now scoped to keyboard focus.
Base UI scrolls the list and reveals the selected item through this part, so the items sitting directly in the popup left it doing that work against a container it doesn't own. The scroll styles move with them.
The popup's background is `rgb(var(--tint-1))`, fully opaque, so a 24px backdrop blur produced nothing visible while still forcing the browser to snapshot and blur the region behind it on every animation frame.
…header Rewriting the copy button dropped `withTooltip`'s default of `true`, which disabled every tooltip whose caller relied on it. The tooltip also lingered after the pointer left, because Base UI keeps a hoverable popup alive while the pointer travels towards it. These are plain labels, so that grace period only reads as lag. The disclosure group still runs on react-aria's `usePress`, which reacts to pointer events, so the select inside its header needs to stop those rather than just clicks.
A profile put 76% of the click in `Recalculate style` inside floating-ui, with a third of that in `get scrollX`. Absolute positioning makes it walk offset parents and read scroll offsets on every measure; the popups are portalled to the body, so `fixed` is safe and skips that. The trigger was also declared as a component inside the render, giving React a fresh type on every pass and remounting the whole subtree. The tooltip carried no z-index, where the app's own overlays all use z-50.
`z-index` only applies to positioned elements. The popup is static inside the positioner, so the rule sat on an element that could never honour it, and the tooltip kept painting behind the operation heading.
|
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Contributor
They ran bespoke keyframes where Popover, DropdownMenu and Tooltip all use `scale-in` / `scale-out`. Same 200ms, but a different curve, so the OpenAPI select never quite matched the popovers next to it.
It kept the document scrollable when react-aria's popover locked body
overflow. The select runs with `modal={false}` now, so nothing locks it —
and `:has()` on `body` makes the browser re-check the whole document on
every DOM mutation.
`InteractiveSection` and `OpenAPIDisclosureGroup` used the low-level hooks for an open/closed boolean, two ARIA attributes and a focus ring. Plain state and native elements cover that, so neither needs a headless library — swapping one for another would have carried the cost without the benefit. The group's trigger hosts the media-type select, so it stays a div with an explicit button role. Now that it handles clicks rather than react-aria's pointer events, the select inside only has to stop propagation once. `react-aria`, `react-aria-components` and `react-stately` leave the package. What the bundle still contains comes from `@scalar/api-client-react`, which ships its own copy inside the lazily loaded Test-it chunk.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #4472 — review that one first.
@gitbook/react-openapiwas the only package still on react-aria, whilepackages/gitbookuses Radix. This starts consolidating both onto Base UI, beginning with the package that carries the weight: an API reference page instantiates around 800 react-aria hooks, andreact-aria-componentsplusreact-statelyaccount for two chunks of 381 KB and 350 KB.Base UI's dependency tree is five packages against react-aria's dozens of
@react-aria/*and@react-stately/*.Migrated
OpenAPITooltipTooltip.Root/Trigger/Portal/Positioner/PopupOpenAPICopyButton<button>— it only used react-aria foronPress, which no caller passedOpenAPISelectSelectOpenAPIDisclosureCollapsibleOpenAPITabsInteractiveSectionandOpenAPIDisclosureGroupstill run on the low-level hooks (useButton,useDisclosure,useDisclosureState, plus react-stately's tree state). They come next, the group heading towardsAccordion.The select changes shape
react-aria cloned the selected item's children into the trigger — which is why the styles carry a
span.react-aria-SelectValue:has(.openapi-statuscode)rule.Select.Valuecan't do that, sochildrenbecomes a render function used for both the list and the trigger, and all eight call sites move with it. The select also owns its tooltip now:Select.Rootrenders no element, so only the select holds the trigger a tooltip can anchor to.Styling
react-aria shipped defaults that Base UI doesn't, so several rules were leaning on it without saying so. Each of these was a real visual defect, none of which the build or typecheck could catch:
data-starting-style/data-ending-styledrive transitions,data-open/data-closeddrive animations. These styles use@keyframes, so they belong on the second pair.z-indexonly applies to positioned elements, so the overlay z-index has to sit on the positioner, not the popup. The tooltip was painting behind the operation heading..react-aria-SelectValuebecomes.openapi-select-value,--trigger-widthbecomes--anchor-width.Two fixes are unrelated to the migration and predate it: the styled select variant hovered to its own resting colour, and the popup carried a 24px backdrop blur behind a fully opaque background — invisible, but still forcing the browser to blur the region on every animation frame.
Positioning
A profile of opening a select put 76% of the click in
Recalculate styleinside floating-ui, a third of that inget scrollX. That's absolute positioning walking offset parents; the popups are portalled to the body, sofixedis safe and skips it. The trigger was also declared as a component inside the render, handing React a new type on every pass.Verifying
Argos already covers OpenAPI pages — Snyk, the GitBook API reference, and several
api-reference/petspages. That's the check that matters here: every defect above was found by eye, and the screenshots are what will catch the next one.