- 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>
14 KiB
Radix -> Base UI migration mapping
Sources: (1) mechanical diff of the 61 component pairs in
apps/v4/registry/bases/{radix,base}/ui/ (ground truth, authored by us),
(2) radix-ui@1.4.3 package exports, (3) base-ui.com docs index for
@base-ui/react@1.6.0. Built 2026-07-02 as the knowledge base for the
migration agent's primitives/ skills.
Coverage matrix
All radix-ui exports, classified for migration:
| Radix primitive | Base UI target | Class |
|---|---|---|
| Accordion | Accordion | direct (Content->Panel) |
| AlertDialog | Alert Dialog | restructured (Overlay->Backdrop, Content->Popup, Cancel->Close, Action dropped) |
| AspectRatio | none | missing: plain div + CSS aspect-ratio (--ratio var) |
| Avatar | Avatar | direct |
| Checkbox | Checkbox | direct (cleanest 1:1) |
| Collapsible | Collapsible | direct (Content->Panel) |
| ContextMenu | Context Menu | restructured (menu mapping) |
| Dialog | Dialog | restructured (Overlay->Backdrop, Content->Popup) |
| DropdownMenu | Menu | RENAMED + restructured (canonical menu mapping) |
| Form | Form + Field + Fieldset | restructured (split into three) |
| HoverCard | Preview Card | RENAMED + positioner model |
| Label | none | missing: native <label> (Field.Label inside forms) |
| Menubar | Menubar + Menu | restructured (menubar root only; menus delegate to Menu) |
| NavigationMenu | Navigation Menu | heavily restructured (Viewport -> Positioner/Popup/Viewport, Indicator->Icon) |
| Popover | Popover | positioner model (Anchor dropped; verify vs docs) |
| Progress | Progress | restructured (new Track/Label/Value parts, no manual transform) |
| RadioGroup | Radio Group + Radio | restructured (Item -> Radio.Root, two subpath imports) |
| ScrollArea | Scroll Area | direct (Scrollbar/Thumb renames) |
| Select | Select | restructured (Viewport->List, ScrollButtons->ScrollArrows, alignItemWithTrigger) |
| Separator | Separator | direct (callable; decorative dropped) |
| Slider | Slider | restructured (Range->Indicator, new Control, thumbAlignment) |
| Switch | Switch | direct (1:1) |
| Tabs | Tabs | direct (Trigger->Tab, Content->Panel) |
| Toast | Toast | restructured (not in our registry pairs; spec from docs; shadcn users mostly use sonner) |
| Toggle | Toggle | direct (callable) |
| ToggleGroup | Toggle Group + Toggle | direct (items use Toggle primitive) |
| Toolbar | Toolbar | direct-ish (not in our pairs; spec from docs) |
| Tooltip | Tooltip | positioner model (delayDuration->delay on Provider) |
| unstable_OneTimePasswordField | OTP Field | from docs (our registry uses input-otp instead) |
| unstable_PasswordToggleField | none | missing: Input + custom toggle |
Utilities:
| Radix utility | Base UI equivalent |
|---|---|
| Slot / asChild | render prop; useRender + mergeProps for the manual Slot idiom |
| Portal | none standalone; per-component Portal parts |
| VisuallyHidden | none; sr-only class |
| AccessibleIcon | none; aria-label + sr-only text |
| Direction | Direction Provider |
Base UI-only (new capabilities, NOT migration targets): Autocomplete, Combobox, Input, Number Field, Checkbox Group, Meter, Filter, CSP Provider.
CORRECTION (dry-run finding): Base UI also ships a Button primitive
(@base-ui/react/button) that supports render. A shadcn button.tsx using
the Slot/asChild idiom migrates to <ButtonPrimitive> directly, NOT to a
hand-rolled useRender wrapper. useRender + mergeProps remains correct for
non-button polymorphic components (breadcrumb link, marker).
Never touched by migration (third-party on both sides): cmdk (command), vaul* (drawer; see drawer section: our base drawer moved vaul -> @base-ui/react/drawer), sonner, input-otp, react-day-picker (calendar), recharts (chart).
Universal patterns (apply across all components)
Imports
Radix appears in TWO import forms; both map to the same Base UI subpath:
- Unified package (current shadcn):
import { X as XPrimitive } from "radix-ui"->import { X as XPrimitive } from "@base-ui/react/<kebab-name>". - Individual packages (legacy/2024-era, e.g. fixture 03):
import * as XPrimitive from "@radix-ui/react-<name>"->import { X as XPrimitive } from "@base-ui/react/<kebab-name>". (The namespace* asimport becomes a named import; remove the individual@radix-ui/react-*package from package.json.) One subpath per component either way. - Types:
React.ComponentProps<typeof XPrimitive.Part>->XPrimitive.Part.Props. Positioner props viaPick<XPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">. - Single-part primitives are callable: radix
XPrimitive.Root->XPrimitive(separator, toggle, toggle-group root, radio-group root, menubar root).
asChild -> render
<Primitive.Close asChild><Button/></Primitive.Close>-><Primitive.Close render={<Button/>}>...</Primitive.Close>.- Manual Slot idiom (
const Comp = asChild ? Slot.Root : "a") ->useRender+mergePropsfrom@base-ui/react/use-render/@base-ui/react/merge-props; prop typeuseRender.ComponentProps<"a">.
Portal / positioning model (biggest structural change)
- Radix:
Portal > Content, positioning props on Content. - Base UI:
Portal > Positioner > Popup.side,sideOffset,align,alignOffset(and select'salignItemWithTrigger) move to Positioner; Popup is the styled box. Positioner conventionally getsisolate z-50. Overlay->Backdrop(dialogs, sheets, drawers). Centered modals (dialog/alert-dialog) use Popup WITHOUT a Positioner.
Data attributes / class hooks
data-[state=open]->data-open;data-[state=closed]->data-closed.- Enter/exit animations:
data-[state=open]:animate-in/data-[state=closed]:animate-out->data-starting-style:*/data-ending-style:*(transition-based, not keyframes). - New Base UI hook:
data-popup-open(open-submenu/trigger marker). - Some triggers gain
aria-disabled:*variants alongsidedisabled:*(accordion, tabs).
CSS custom properties
--radix-<comp>-content-transform-origin->--transform-origin--radix-<comp>-content-available-height->--available-height--radix-<comp>-trigger-width->--anchor-width--radix-accordion-content-height->--accordion-panel-height- nav-menu
--radix-navigation-menu-viewport-height/width->--positioner-height/width,--popup-height/width,--available-width
Props
- Tooltip Provider:
delayDuration->delay. - Select:
position="popper"|"item-aligned"->alignItemWithTriggerboolean. - Slider: gains
thumbAlignment("edge");Range->Indicator+ newControl. - Navigation Menu:
viewportboolean dropped;alignforwarded to Positioner. value/defaultValue/onOpenChangesignatures pass through unchanged at the wrapper level (verify per-primitive callback signatures against docs when authoring specs; wrappers do not exercise them all).
Part-rename quick reference
| radix part | Base UI part |
|---|---|
*.Root (single-part comps) |
callable *Primitive |
Overlay |
Backdrop |
Content (overlay comps) |
Popup (inside Positioner) |
Content (accordion/collapsible/tabs) |
Panel |
tabs Trigger |
Tab |
menu Label |
GroupLabel |
menu ItemIndicator |
CheckboxItemIndicator / RadioItemIndicator |
Sub / SubTrigger |
SubmenuRoot / SubmenuTrigger |
slider Range |
Indicator (+ new Control) |
select Viewport |
List |
select ScrollUp/DownButton |
ScrollUp/DownArrow |
scroll-area ScrollAreaScrollbar / ScrollAreaThumb |
Scrollbar / Thumb |
nav-menu Indicator |
Icon |
nav-menu Viewport |
Positioner > Popup > Viewport |
hover-card HoverCard* |
PreviewCard* |
radio-group Item / Indicator |
Radio.Root / Radio.Indicator |
popover Anchor |
dropped (verify against docs) |
alert-dialog Cancel / Action |
Close / dropped (plain Button) |
separator decorative prop |
dropped |
| Label primitive | native <label> |
Per-component notes
accordion
Root/Item/Header/Trigger same; Content -> Panel. Trigger disabled:* ->
aria-disabled:*. Height var -> --accordion-panel-height; add
data-starting-style:h-0 data-ending-style:h-0.
dialog / alert-dialog / sheet
Overlay -> Backdrop, Content -> Popup, Close kept (asChild -> render).
Alert-dialog: Cancel -> Close; Action has no primitive (plain Button).
Sheet: slide animations rewritten from animate-in/out to
data-starting-style / data-ending-style with explicit translate per
data-[side=...]. Centered modals: no Positioner.
drawer (vaul -> Base UI) — OPT-IN ONLY, not part of a radix migration
Vaul is NOT radix: during a radix -> base-ui migration, leave drawer.tsx
untouched and report it (hard rule in SKILL.md). This mapping exists only for
when the user EXPLICITLY asks to also move their drawer off vaul.
Root gains modal, snapPoints, swipeDirection (default "down"),
showSwipeHandle. Content (single) -> Viewport > Popup > Content.
data-[vaul-drawer-direction=...] -> data-[swipe-direction=...] /
data-[swipe-axis=...] + --drawer-* vars. New SwipeHandle part and a
context provider in our wrapper. This is a vaul migration, not radix.
popover / tooltip / hover-card
Portal > Positioner > Popup. Popover: Anchor dropped, Title is now a real
primitive part. Tooltip: Provider delayDuration -> delay; Content gains
side/align/alignOffset; default sideOffset 0 -> 4; Arrow gets explicit
per-side positioning classes. HoverCard: primitive renamed PreviewCard
(public wrapper names stay HoverCard*).
menus (dropdown-menu -> Menu; context-menu; menubar)
Canonical mapping: Label -> GroupLabel, ItemIndicator ->
CheckboxItemIndicator/RadioItemIndicator, Sub -> SubmenuRoot, SubTrigger ->
SubmenuTrigger, Content -> Portal > Positioner > Popup, SubContent rebuilt
from the Content component. Content hoists align/alignOffset/side/sideOffset.
SubTrigger open marker: data-popup-open. Context-menu has its own subpath
(@base-ui/react/context-menu), same anatomy. Menubar: only the root and
checkbox/radio items are menubar/menu primitives; everything else delegates
to the Menu wrappers (radix Menubar.Menu -> Menu.Root).
select
Label -> GroupLabel, Viewport -> List, ScrollUp/DownButton ->
ScrollUp/DownArrow. Icon/ItemIndicator go asChild -> render.
position -> alignItemWithTrigger (default true) on Positioner. Vars ->
--available-height / --anchor-width / --transform-origin.
form controls
Checkbox: 1:1. Switch: 1:1. Radio group: group from
@base-ui/react/radio-group (callable), items from @base-ui/react/radio
(Radio.Root + Radio.Indicator). Slider: Root > Control > Track > Indicator + Thumbs, thumbAlignment="edge"; layout classes move Root ->
Control. Toggle/toggle-group: callable primitives; group items reuse Toggle.
tabs / collapsible / progress / separator / scroll-area / label
Tabs: Trigger -> Tab, Content -> Panel, aria-disabled:* added. Collapsible:
Content -> Panel. Progress: new Track/Label/Value parts; primitive computes
fill (drop the manual translateX). Separator: callable, decorative dropped.
Scroll-area: Scrollbar/Thumb renames only. Label: no primitive; native
<label>.
navigation-menu
Viewport moves out of Root into Portal > Positioner > Popup > Viewport
(our NavigationMenuPositioner). Indicator -> Icon. viewport boolean prop
removed; align forwarded to Positioner. New data-instant,
data-activation-direction hooks; vars -> --positioner-height/width,
--popup-height/width.
breadcrumb / marker (Slot users)
Slot.Root + asChild -> useRender + mergeProps
(useRender.ComponentProps<"a">, render prop, state.slot).
Doc-validation TODOs (before specs are final)
- Popover Anchor: confirm Base UI has no anchor equivalent (Positioner may
accept an
anchorprop; our wrapper simply dropped the part). - Callback signatures: radix
onOpenChange(open)vs Base UIonOpenChange(open, event, reason)style differences; wrappers pass through so the pair diff cannot see them. Check per primitive. - Toast, Toolbar, Form/Field/Fieldset, OTP Field: not covered by our pairs; author these specs from docs alone.
- Controlled-prop names on menus/select (
open,value,highlighted) and anydefaultChecked/checkednuances. - Focus/dismissal behavior knobs (
onInteractOutside,onEscapeKeyDown-> Base UI equivalents) which our wrappers do not surface.
Slot -> useRender: WORKED EXAMPLE (avoid the mergeProps pitfall)
Radix:
import { Slot } from "radix-ui"
function BreadcrumbLink({ asChild, className, ...props }: React.ComponentProps<"a"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "a"
return <Comp data-slot="breadcrumb-link" className={cn("...", className)} {...props} />
}
Base UI:
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
function BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<"a">) {
return useRender({
defaultTagName: "a",
render,
props: mergeProps<"a">(
// PITFALL: data-* attributes fail excess-property checking when passed
// as an object literal into mergeProps (they are only special-cased in
// JSX). Cast the literal:
{ "data-slot": "breadcrumb-link", className: cn("...", className) } as React.ComponentProps<"a">,
props
),
})
}
Two rules:
- This pattern is ONLY for non-button polymorphic components (breadcrumb
link, marker, badge, item...).
button.tsxmigrates to the real@base-ui/react/buttonprimitive, which acceptsrendernatively. - Always cast object literals containing
data-*keys passed tomergeProps(as React.ComponentProps<"tag">), or tsc fails on every one.
Positioner props: Pick means FORWARD
When a wrapper exposes positioning props via
Pick<XPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">,
you MUST destructure each of those props in the wrapper and pass them to
<XPrimitive.Positioner> explicitly. If you forget, they fall through
...props onto the Popup (wrong DOM node) and positioning silently breaks.
No JSX-level type error catches this; only the wrapper's own destructuring
discipline and a browser check do. Checklist per overlay wrapper:
declare -> destructure -> forward. All three, every time.