Files
kolaytercih/.agents/skills/migrate-radix-to-base/overlays.md
bilalgursen 87f265ec15 Build TercihAI MVP: landing page, YÖK Atlas data pipeline, rank-based results
- Add product vision/market research doc (VISION.md)
- Set up shadcn/ui (radix + nova preset) with blue/orange theme, Outfit + Work Sans fonts
- Landing page: hero with rank input, problem/steps sections, comparison table, FAQ
- Data pipeline: CSV archive ingest (2021-2024) + live YÖK Atlas API refresh (2025)
  into SQLite (npm run ingest / refresh); zeros normalized to NULL
- /sonuc page: hayal/dengeli/garanti buckets by COALESCE(sira2025, sira2024),
  score-type switcher, 2024→2025 trend indicators
- Add ui-ux-pro-max and shadcn agent skills, shadcn MCP config, launch.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:46:53 +03:00

31 KiB

Radix UI → Base UI props migration mapping (overlays)

Components covered: dialog, alert-dialog, popover, tooltip, hover-card (Base UI: preview-card).

Sources: radix-ui.com primitives docs (fetched 2026-07-02) and base-ui.com /react/components/*.md endpoints (fetched 2026-07-02).

Global conventions that apply to every component below:

  • asChild (Radix, every part) → render (Base UI, every part). Radix: asChild?: boolean merges props onto the single child. Base UI: render?: ReactElement | ((props: HTMLProps, state: Part.State) => ReactElement). For buttons replaced with non-button elements, also set nativeButton={false}.
  • onOpenChange signature changed everywhere. Radix: (open: boolean) => void. Base UI: (open: boolean, eventDetails: X.Root.ChangeEventDetails) => void where eventDetails is { reason, event, trigger, cancel(), allowPropagation(), isCanceled, isPropagationAllowed, preventUnmountOnClose() }.
  • Radix per-interaction dismiss callbacks (onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside) have NO 1:1 Base UI props. They are replaced by onOpenChange's eventDetails.reason ('escape-key', 'outside-press', 'focus-out') + eventDetails.cancel() to prevent the close (the equivalent of Radix event.preventDefault()).
  • forceMount (Radix, Portal/Overlay/Content) → keepMounted on Base UI Portal only (boolean, default false). For exit animations Base UI does not need it: it holds the popup mounted itself and exposes data-starting-style / data-ending-style, onOpenChangeComplete, and actionsRef.current.unmount() for externally-controlled animations.
  • Base UI className and style accept state callbacks ((state) => ...) on every rendered part.
  • Radix [data-state="open" | "closed"] → Base UI presence attributes data-open / data-closed.

dialog

Part mapping: Root→Root, Trigger→Trigger, Portal→Portal, Overlay→Backdrop, Content→Popup (centered modal: no Positioner), Title→Title, Description→Description, Close→Close.

Root → Root

Radix prop Type / default Base UI equivalent Migration note
defaultOpen boolean / - (Base default false) defaultOpen Same.
open boolean / - open Same.
onOpenChange (open: boolean) => void / - onOpenChange Signature changed: (open: boolean, eventDetails: Dialog.Root.ChangeEventDetails) => void. Reasons: 'trigger-press' | 'outside-press' | 'escape-key' | 'close-press' | 'focus-out' | 'imperative-action' | 'none'.
modal boolean / true modal Widened: boolean | 'trap-focus', default true. 'trap-focus' traps focus without scroll lock / outside-pointer blocking.

Trigger → Trigger

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render render={<MyButton />}; add nativeButton={false} if the rendered element is not a <button>.

Portal → Portal

Radix prop Type / default Base UI equivalent Migration note
forceMount boolean / - keepMounted Renamed + inverted intent: keepMounted?: boolean (default false) keeps the portal in the DOM while hidden. Usually droppable; Base UI keeps the popup mounted during exit animations automatically.
container HTMLElement / document.body container Same name, wider type: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null. Note: Base UI Portal renders a <div> wrapper (Radix Portal renders nothing extra per child).

Overlay → Backdrop

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Same pattern.
forceMount boolean / - dropped Backdrop stays mounted through exit animations natively; use Portal keepMounted if you need always-mounted DOM. Base-only: forceRender (boolean, default false) forces the backdrop to render even when the dialog is nested.

Content → Popup

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Same pattern.
forceMount boolean / - dropped See Portal keepMounted + Root actionsRef.unmount() + onOpenChangeComplete.
onOpenAutoFocus (event: Event) => void / - moved to Popup initialFocus Signature changed. Radix: prevent via event.preventDefault(). Base: initialFocus?: boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null). false = don't move focus; ref/element = focus target; function receives 'mouse' | 'touch' | 'pen' | 'keyboard'.
onCloseAutoFocus (event: Event) => void / - moved to Popup finalFocus Same shape as initialFocus but for close (closeType: InteractionType).
onEscapeKeyDown (event: KeyboardEvent) => void / - moved to Root onOpenChange if (eventDetails.reason === 'escape-key') eventDetails.cancel() replaces event.preventDefault().
onPointerDownOutside (event: PointerDownOutsideEvent) => void / - moved to Root onOpenChange Reason 'outside-press'; cancel with eventDetails.cancel(). Declarative shortcut: Root disablePointerDismissal (boolean, default false).
onInteractOutside (event: PointerDownOutsideEvent | FocusOutsideEvent) => void / - moved to Root onOpenChange Covers reasons 'outside-press' and 'focus-out' (focus-out applies to non-modal dialogs).

Title → Title / Description → Description / Close → Close

Radix prop Type / default Base UI equivalent Migration note
asChild (Title) boolean / false render Base Title renders <h2>.
asChild (Description) boolean / false render Base Description renders <p>.
asChild (Close) boolean / false render Base Close renders <button>; nativeButton available.

Base UI only props worth knowing (dialog)

  • Root: actionsRef (RefObject<{ unmount(), close() }>), onOpenChangeComplete: (open: boolean) => void, disablePointerDismissal, modal: 'trap-focus', handle / triggerId / defaultTriggerId + Dialog.createHandle() (detached and multiple triggers), children as payload render function.
  • Trigger: payload, handle, id, nativeButton.
  • Popup: initialFocus, finalFocus.
  • Backdrop: forceRender.
  • New part: Viewport (scrollable positioning container for the popup, useful for outside-scroll dialogs).

Data attributes (dialog)

Radix Base UI Where
[data-state="open"] data-open Backdrop, Popup, Viewport (presence attr).
[data-state="closed"] data-closed Backdrop, Popup, Viewport.
[data-state] on Trigger data-popup-open Trigger (presence attr).
- data-disabled Trigger, Close.
- data-starting-style / data-ending-style Backdrop, Popup, Viewport; hooks for enter/exit CSS transitions (replaces Radix animate-on-data-state idiom).
- data-nested, data-nested-dialog-open Popup, Viewport.

CSS variables (dialog)

Radix Base UI
(none documented) --nested-dialogs (number, on Popup): count of dialogs nested within.

alert-dialog

Part mapping: Root→Root, Trigger→Trigger, Portal→Portal, Overlay→Backdrop, Content→Popup, Title→Title, Description→Description, Cancel→Close, Action→NO primitive (render a plain button; close via controlled state, Root actionsRef.close(), or reuse AlertDialog.Close with action semantics in the wrapper). Base UI AlertDialog is always modal and never closes on outside press by default (no modal prop, reasons still include 'outside-press'/'focus-out' in the type but pointer dismissal is disabled by design).

Root → Root

Radix prop Type / default Base UI equivalent Migration note
defaultOpen boolean / - (Base default false) defaultOpen Same.
open boolean / - open Same.
onOpenChange (open: boolean) => void / - onOpenChange Signature changed: (open: boolean, eventDetails: AlertDialog.Root.ChangeEventDetails) => void. Same reason union as dialog.

(Radix AlertDialog.Root has no modal prop; Base UI AlertDialog.Root also has none. Parity.)

Trigger → Trigger

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Plus nativeButton, payload, handle, id.

Portal → Portal

Radix prop Type / default Base UI equivalent Migration note
forceMount boolean / - keepMounted Same as dialog.
container HTMLElement / document.body container Same as dialog (wider type, renders a <div>).

Overlay → Backdrop

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Same pattern.
forceMount boolean / - dropped Base-only forceRender exists for nested cases.

Content → Popup

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Same pattern.
forceMount boolean / - dropped See dialog notes.
onOpenAutoFocus (event: Event) => void / - moved to Popup initialFocus Same semantics as dialog. Note: Radix alert-dialog focuses Cancel by default; Base UI focuses the first tabbable element. To preserve Radix behavior pass initialFocus={cancelRef}.
onCloseAutoFocus (event: Event) => void / - moved to Popup finalFocus Same as dialog.
onEscapeKeyDown (event: KeyboardEvent) => void / - moved to Root onOpenChange Reason 'escape-key' + eventDetails.cancel().

(Radix AlertDialog.Content intentionally has no onPointerDownOutside/onInteractOutside; nothing to map.)

Title → Title / Description → Description

Same as dialog: asChildrender. Title renders <h2>, Description renders <p>.

Cancel → Close

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render on AlertDialog.Close Renamed part. nativeButton available. Radix's "Cancel receives focus on open" default must be recreated with Popup initialFocus.

Action → (no primitive)

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false dropped No Base UI part. Wrapper renders a styled <button>; close after the action via controlled open, actionsRef.current.close(), or by composing AlertDialog.Close and running the action in onClick.

Base UI only props worth knowing (alert-dialog)

  • Root: actionsRef, onOpenChangeComplete, handle / triggerId / defaultTriggerId + AlertDialog.createHandle(), payload-render children.
  • Popup: initialFocus, finalFocus.
  • Backdrop: forceRender. New part: Viewport.

Data attributes (alert-dialog)

Identical table to dialog: data-state="open"/"closed"data-open/data-closed (Backdrop, Popup, Viewport); Trigger data-statedata-popup-open; Base-only data-disabled (Trigger, Close), data-starting-style, data-ending-style, data-nested, data-nested-dialog-open.

CSS variables (alert-dialog)

Radix Base UI
(none documented) --nested-dialogs (number, on Popup).

popover

Part mapping: Root→Root, Trigger→Trigger, Anchor→(Positioner anchor prop), Portal→Portal, Content→Portal>Positioner>Popup (positioning props move to Positioner; focus/dismiss concerns split between Popup and Root), Close→Close, Arrow→Arrow. Base UI also has Backdrop, Title, Description, Viewport parts with no Radix counterpart.

Root → Root

Radix prop Type / default Base UI equivalent Migration note
defaultOpen boolean / - (Base default false) defaultOpen Same.
open boolean / - open Same.
onOpenChange (open: boolean) => void / - onOpenChange Signature changed: (open: boolean, eventDetails: Popover.Root.ChangeEventDetails) => void. Reasons add hover/focus: 'trigger-hover' | 'trigger-focus' | 'trigger-press' | 'outside-press' | 'escape-key' | 'close-press' | 'focus-out' | 'imperative-action' | 'none'.
modal boolean / false modal Widened: boolean | 'trap-focus', default false. When true, focus trapping requires a Popover.Close inside the Popup (can be sr-only).

Trigger → Trigger

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Plus nativeButton. Base-only on Trigger: openOnHover (false), delay (300), closeDelay (0), payload, handle, id.

Anchor → Positioner anchor prop

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false dropped (part removed) No Anchor part. Pass anchor to Positioner: Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null. Default anchor is the trigger.

Portal → Portal

Radix prop Type / default Base UI equivalent Migration note
forceMount boolean / - keepMounted boolean, default false.
container HTMLElement / document.body container Wider type (adds ShadowRoot/RefObject); Portal renders a <div>.

Content → Positioner + Popup

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render (Popup) Same pattern.
onOpenAutoFocus (event: Event) => void / - moved to Popup initialFocus Same shape as dialog (boolean | RefObject | (openType: InteractionType) => ...).
onCloseAutoFocus (event: Event) => void / - moved to Popup finalFocus Same shape.
onEscapeKeyDown (event: KeyboardEvent) => void / - moved to Root onOpenChange Reason 'escape-key' + eventDetails.cancel().
onPointerDownOutside (event: PointerDownOutsideEvent) => void / - moved to Root onOpenChange Reason 'outside-press' + eventDetails.cancel().
onFocusOutside (event: FocusOutsideEvent) => void / - moved to Root onOpenChange Reason 'focus-out' + eventDetails.cancel().
onInteractOutside (event: PointerDownOutsideEvent | FocusOutsideEvent) => void / - moved to Root onOpenChange Handle both 'outside-press' and 'focus-out'.
forceMount boolean / - dropped Use Portal keepMounted, onOpenChangeComplete, actionsRef.unmount().
side "top" | "right" | "bottom" | "left" / "bottom" moved to Positioner side Type is Side which adds logical values 'inline-start' | 'inline-end'. Default 'bottom' (same).
sideOffset number / 0 moved to Positioner sideOffset Widened: number | OffsetFunction where the function receives { anchor, positioner, side, align }. Default 0 (same).
align "start" | "center" | "end" / "center" moved to Positioner align Same values/default.
alignOffset number / 0 moved to Positioner alignOffset Widened: number | OffsetFunction. Default 0 (same).
avoidCollisions boolean / true moved to Positioner collisionAvoidance Signature changed. Radix boolean → Base CollisionAvoidance object { side?: 'flip' | 'shift' | 'none'; align?: 'flip' | 'shift' | 'none'; fallbackAxisSide?: 'start' | 'end' | 'none' }. avoidCollisions={false}collisionAvoidance={{ side: 'none', align: 'none', fallbackAxisSide: 'none' }}.
collisionBoundary Boundary (Element | null | Array<Element | null>) / [] moved to Positioner collisionBoundary Same name; Base Boundary defaults to 'clipping-ancestors' (Radix default = viewport/clipping ancestors via []).
collisionPadding number | Padding / 0 moved to Positioner collisionPadding Same shape; default changes 05.
arrowPadding number / 0 moved to Positioner arrowPadding Same; default changes 05.
sticky "partial" | "always" / "partial" dropped (repurposed name) Radix sticky governed alignment-axis sticking; Base equivalent is collisionAvoidance.align ('shift' ≈ sticky behavior). CAUTION: Base UI Positioner has a sticky: boolean (default false) prop with a DIFFERENT meaning: keep the popup in the viewport after the anchor scrolls out of view. Do not copy the Radix value across.
hideWhenDetached boolean / false dropped (with workaround) Positioner/Popup expose data-anchor-hidden when the anchor is hidden; recreate with CSS: [data-anchor-hidden] { visibility: hidden } on the Positioner.

Close → Close

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Plus nativeButton.

Arrow → Arrow

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Radix renders an <svg>; Base renders a <div> — supply your own SVG child.
width number / 10 dropped Size the arrow element/SVG via CSS.
height number / 5 dropped Size via CSS. Arrow must be a child of Popup (inside Positioner).

Base UI only props worth knowing (popover)

  • Root: actionsRef, onOpenChangeComplete, handle / triggerId / defaultTriggerId + Popover.createHandle(), payload-render children, modal: 'trap-focus'.
  • Trigger: openOnHover + delay + closeDelay (hover-open popovers), payload, nativeButton, id.
  • Positioner: positionMethod ('absolute' \| 'fixed'), disableAnchorTracking, anchor, collisionAvoidance.
  • Popup: initialFocus, finalFocus.
  • New parts: Backdrop, Title, Description, Viewport (animated content swaps between multiple triggers).

Data attributes (popover)

Radix (on Content/Trigger/Arrow) Base UI Where
[data-state="open"/"closed"] data-open / data-closed Backdrop, Positioner, Popup, Arrow.
[data-state] on Trigger data-popup-open Trigger. Base also adds data-pressed.
[data-side] "left" | "right" | "bottom" | "top" data-side Positioner, Popup, Arrow; values extended with 'inline-start' | 'inline-end'.
[data-align] "start" | "end" | "center" data-align Positioner, Popup, Arrow.
- data-starting-style / data-ending-style Popup, Backdrop (enter/exit animation hooks).
- data-anchor-hidden Positioner (replaces hideWhenDetached).
- data-instant ('click' | 'dismiss' | 'focus' | 'trigger-change') Popup.
- data-uncentered Arrow (arrow can't center on anchor).

CSS variables (popover)

Radix (on Content) Base UI (on Positioner unless noted)
--radix-popover-content-transform-origin --transform-origin
--radix-popover-content-available-width --available-width
--radix-popover-content-available-height --available-height
--radix-popover-trigger-width --anchor-width
--radix-popover-trigger-height --anchor-height
- --positioner-width / --positioner-height (Positioner), --popup-width / --popup-height (Popup, and on Viewport's previous container)

tooltip

Part mapping: Provider→Provider, Root→Root, Trigger→Trigger, Portal→Portal, Content→Portal>Positioner>Popup, Arrow→Arrow. Delay control moves: Radix delayDuration lives on Provider/Root; Base UI open/close delays live on Provider (delay/closeDelay) and Trigger (delay/closeDelay).

Provider → Provider

Radix prop Type / default Base UI equivalent Migration note
delayDuration number / 700 delay Renamed. Base has no documented default on Provider (Trigger default is 600).
skipDelayDuration number / 300 timeout Renamed + semantics kept: another tooltip opens instantly if the previous closed within timeout ms. Default 300400.
disableHoverableContent boolean / - dropped at Provider; see Root disableHoverablePopup Base UI equivalent exists only per-Root (renamed).

Root → Root

Radix prop Type / default Base UI equivalent Migration note
defaultOpen boolean / - (Base default false) defaultOpen Same.
open boolean / - open Same.
onOpenChange (open: boolean) => void / - onOpenChange Signature changed: (open: boolean, eventDetails: Tooltip.Root.ChangeEventDetails) => void. Reasons: 'trigger-hover' | 'trigger-focus' | 'trigger-press' | 'outside-press' | 'escape-key' | 'disabled' | 'imperative-action' | 'none'.
delayDuration number / 700 moved to Trigger delay number, default 600. closeDelay (default 0) is also on Trigger.
disableHoverableContent boolean / - disableHoverablePopup Renamed; boolean, default false.

Trigger → Trigger

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Base-only on Trigger: delay (600), closeDelay (0), closeOnClick (true), disabled (false), payload, handle.

Portal → Portal

Radix prop Type / default Base UI equivalent Migration note
forceMount boolean / - keepMounted boolean, default false.
container HTMLElement / document.body container Wider type; renders a <div>.

Content → Positioner + Popup

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render (Popup) Same pattern.
aria-label string / - dropped (plain DOM attr) Pass aria-label straight through to Popup if needed; no special prop.
onEscapeKeyDown (event: KeyboardEvent) => void / - moved to Root onOpenChange Reason 'escape-key' + eventDetails.cancel().
onPointerDownOutside (event: PointerDownOutsideEvent) => void / - moved to Root onOpenChange Reason 'outside-press' + eventDetails.cancel().
forceMount boolean / - dropped Portal keepMounted / actionsRef.unmount().
side enum / "top" moved to Positioner side Side (adds 'inline-start' | 'inline-end'); default 'top' (same).
sideOffset number / 0 moved to Positioner sideOffset number | OffsetFunction; default 0.
align enum / "center" moved to Positioner align Same values/default.
alignOffset number / 0 moved to Positioner alignOffset number | OffsetFunction; default 0.
avoidCollisions boolean / true moved to Positioner collisionAvoidance Same conversion as popover (false → all-'none' object).
collisionBoundary Boundary / [] moved to Positioner collisionBoundary Base default 'clipping-ancestors'.
collisionPadding number | Padding / 0 moved to Positioner collisionPadding Default 05.
arrowPadding number / 0 moved to Positioner arrowPadding Default 05.
sticky "partial" | "always" / "partial" dropped (repurposed name) Same caveat as popover: Base sticky: boolean means "stay in viewport when anchor scrolls away"; alignment sticking is collisionAvoidance.align.
hideWhenDetached boolean / false dropped (with workaround) Style [data-anchor-hidden] on Positioner.

Arrow → Arrow

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Base renders <div>; bring your own SVG.
width number / 10 dropped CSS sizing.
height number / 5 dropped CSS sizing.

Base UI only props worth knowing (tooltip)

  • Root: trackCursorAxis ('none' \| 'x' \| 'y' \| 'both', default 'none'), disabled, disableHoverablePopup, actionsRef, onOpenChangeComplete, handle / triggerId / defaultTriggerId + Tooltip.createHandle(), payload-render children.
  • Provider: closeDelay (shared close delay, no Radix counterpart).
  • Trigger: closeOnClick, disabled, delay, closeDelay, payload.
  • Positioner: positionMethod, disableAnchorTracking, anchor, collisionAvoidance.
  • New part: Viewport.

Data attributes (tooltip)

Radix Base UI Where
[data-state] "closed" | "delayed-open" | "instant-open" (Content) data-open / data-closed + data-instant ('delay' | 'dismiss' | 'focus') Popup, Arrow, Positioner (open/closed). The delayed/instant distinction becomes the data-instant value.
[data-state] (Trigger) data-popup-open Trigger. Base also adds data-trigger-disabled.
[data-side] data-side Positioner, Popup, Arrow; adds 'inline-start' | 'inline-end'.
[data-align] data-align Positioner, Popup, Arrow.
- data-starting-style / data-ending-style Popup.
- data-anchor-hidden Positioner.
- data-uncentered Arrow.

CSS variables (tooltip)

Radix (on Content) Base UI (on Positioner)
--radix-tooltip-content-transform-origin --transform-origin
--radix-tooltip-content-available-width --available-width
--radix-tooltip-content-available-height --available-height
--radix-tooltip-trigger-width --anchor-width
--radix-tooltip-trigger-height --anchor-height
- --popup-width / --popup-height (Viewport's previous container).

hover-card → preview-card

Part mapping: HoverCard.Root→PreviewCard.Root, Trigger→Trigger, Portal→Portal, Content→Portal>Positioner>Popup, Arrow→Arrow. Delays move from Root to Trigger. Both libraries render the trigger as an <a> element.

Root → Root

Radix prop Type / default Base UI equivalent Migration note
defaultOpen boolean / - (Base default false) defaultOpen Same.
open boolean / - open Same.
onOpenChange (open: boolean) => void / - onOpenChange Signature changed: (open: boolean, eventDetails: PreviewCard.Root.ChangeEventDetails) => void. Reasons: 'trigger-hover' | 'trigger-focus' | 'trigger-press' | 'outside-press' | 'escape-key' | 'imperative-action' | 'none'.
openDelay number / 700 moved to Trigger delay Renamed + moved; default changes 700600.
closeDelay number / 300 moved to Trigger closeDelay Moved; default 300 (same).

Trigger → Trigger

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Base Trigger renders <a>; Base-only: delay (600), closeDelay (300), payload, handle. No nativeButton (it is a link, not a button).

Portal → Portal

Radix prop Type / default Base UI equivalent Migration note
forceMount boolean / - keepMounted boolean, default false.
container HTMLElement / document.body container Wider type; renders a <div>.

Content → Positioner + Popup

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render (Popup) Same pattern.
forceMount boolean / - dropped Portal keepMounted / actionsRef.unmount() / onOpenChangeComplete.
side enum / "bottom" moved to Positioner side Side (adds 'inline-start' | 'inline-end'); default 'bottom' (same).
sideOffset number / 0 moved to Positioner sideOffset number | OffsetFunction; default 0.
align enum / "center" moved to Positioner align Same values/default.
alignOffset number / 0 moved to Positioner alignOffset number | OffsetFunction; default 0.
avoidCollisions boolean / true moved to Positioner collisionAvoidance Same conversion as popover.
collisionBoundary Boundary / [] moved to Positioner collisionBoundary Base default 'clipping-ancestors'.
collisionPadding number | Padding / 0 moved to Positioner collisionPadding Default 05.
arrowPadding number / 0 moved to Positioner arrowPadding Default 05.
sticky "partial" | "always" / "partial" dropped (repurposed name) Same caveat as popover/tooltip.
hideWhenDetached boolean / false dropped (with workaround) Style [data-anchor-hidden] on Positioner.

(Radix HoverCard.Content documents no dismiss callbacks; escape/outside dismissal maps to Root onOpenChange reasons 'escape-key' / 'outside-press' if needed.)

Arrow → Arrow

Radix prop Type / default Base UI equivalent Migration note
asChild boolean / false render Base renders <div>.
width number / 10 dropped CSS sizing.
height number / 5 dropped CSS sizing.

Base UI only props worth knowing (preview-card)

  • Root: actionsRef, onOpenChangeComplete, handle / triggerId / defaultTriggerId + PreviewCard.createHandle(), payload-render children.
  • Trigger: payload, handle, per-trigger delay/closeDelay.
  • Positioner: positionMethod, disableAnchorTracking, anchor, collisionAvoidance.
  • New parts: Backdrop, Viewport.

Data attributes (preview-card)

Radix Base UI Where
[data-state="open"/"closed"] data-open / data-closed Backdrop, Positioner, Popup, Arrow.
[data-state] (Trigger) data-popup-open Trigger.
[data-side] data-side Positioner, Popup, Arrow; adds 'inline-start' | 'inline-end'.
[data-align] data-align Positioner, Popup, Arrow.
- data-starting-style / data-ending-style Popup, Backdrop.
- data-anchor-hidden Positioner.
- data-uncentered Arrow.

CSS variables (preview-card)

Radix (on Content) Base UI (on Positioner)
--radix-hover-card-content-transform-origin --transform-origin
--radix-hover-card-content-available-width --available-width
--radix-hover-card-content-available-height --available-height
--radix-hover-card-trigger-width --anchor-width
--radix-hover-card-trigger-height --anchor-height
- --popup-width / --popup-height (Viewport's previous container).