packages/coss-ui/radix_shadcn_migration_guide.md
This guide is designed for developers who already have applications built with shadcn/ui or Radix UI and want to adopt coss ui components. coss ui is fundamentally built with Base UI from the ground up—it is not an adaptation from Radix UI. Recognizing that many teams are migrating from Radix-based libraries, this page consolidates all migration instructions for a smooth transition.
coss ui components are built on Base UI, not Radix. This means the architecture and API patterns are different by design. However, we've worked to make the migration path as clear as possible by:
The most common change across all components is replacing Radix UI's asChild prop with Base UI's render prop:
Some components have updated names for clarity and consistency:
*Content → *Popup or *Panel (e.g., DialogContent → DialogPopup)Quick Checklist:
type="multiple" → multiple={true} on Accordiontype="single" from Accordioncollapsible from AccordiondefaultValueAccordionPanel going forward; AccordionContent remains for legacyasChild on parts, switch to the render propProp Mapping:
| Component | Radix UI Prop | Base UI Prop |
|---|---|---|
Accordion | type (enum, "single" or "multiple") | multiple (boolean, default: false) |
Accordion | collapsible | removed |
Comparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:type="multiple"] // [!code word:collapsible] // [!code word:"item-1":1] <Accordion type="multiple" collapsible defaultValue="item-1"> <AccordionItem value="item-1"> <AccordionTrigger>Title</AccordionTrigger> <AccordionContent>Content</AccordionContent> </AccordionItem> </Accordion> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:multiple={true}] // [!code word:\{\["item-1"\]\}] <Accordion multiple={true} defaultValue={["item-1"]}> <AccordionItem value="item-1"> <AccordionTrigger>Title</AccordionTrigger> <AccordionPanel>Content</AccordionPanel> </AccordionItem> </Accordion> ``` </span>New Variants:
We've added new colored variants for better semantic meaning:
| Variant | Description |
|---|---|
info | Displays an info alert (blue) |
success | Displays a success alert (green) |
warning | Displays a warning alert (yellow) |
error | Displays a error alert (red) |
Ensure you have the following variables imported in your CSS file:
--destructive-foreground--info--info-foreground--success--success-foreground--warning--warning-foregroundQuick Checklist:
asChild → render on AlertDialogTrigger and closing buttonsAlertDialogAction and AlertDialogCancel → AlertDialogCloseAlertDialogPopup; AlertDialogContent remains for legacyAlertDialogPanel to wrap main content between AlertDialogHeader and AlertDialogFooterasChild on any other parts, switch to the render propComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] // [!code word:AlertDialogCancel] // [!code word:AlertDialogAction] <AlertDialog> <AlertDialogTrigger asChild> <Button variant="outline">Show Alert Dialog</Button> </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction asChild> <Button>Continue</Button> </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] // [!code word:AlertDialogClose] // [!code word:<AlertDialogPanel>Content</AlertDialogPanel>] <AlertDialog> <AlertDialogTrigger render={<Button variant="outline" />}> Show Alert Dialog </AlertDialogTrigger> <AlertDialogPopup> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogPanel>Content</AlertDialogPanel> <AlertDialogFooter> <AlertDialogClose render={<Button variant="ghost" />}> Cancel </AlertDialogClose> <AlertDialogClose render={<Button variant="destructive" />}> Continue </AlertDialogClose> </AlertDialogFooter> </AlertDialogPopup> </AlertDialog> ``` </span>Quick Checklist:
asChild → render on AvatarComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Avatar asChild> <Link href="/profile"> <AvatarImage src="avatar.jpg" alt="User" /> <AvatarFallback>U</AvatarFallback> </Link> </Avatar> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] <Avatar render={<Link href="/profile" />}> <AvatarImage src="avatar.jpg" alt="User" /> <AvatarFallback>U</AvatarFallback> </Avatar> ``` </span>Quick Checklist:
asChild → render on BadgeSize Comparison:
Compared to shadcn/ui, our Badge component includes size variants for better density control. shadcn/ui badges have a fixed size, while our component offers flexible sizing with sm, default, and lg options.
So, if you want to preserve the original shadcn/ui badge size, you should use the lg size in coss ui.
New Variants:
We've added new colored variants to the existing ones (default, destructive, outline, secondary) for better semantic meaning and visual communication:
| Variant | Description |
|---|---|
info | Blue badge for information |
success | Green badge for success states |
warning | Yellow badge for warnings |
error | Red badge for errors |
Ensure you have the following variables imported in your CSS file:
--destructive-foreground--info--info-foreground--success--success-foreground--warning--warning-foregroundComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Badge asChild> <Link href="/new">New</Link> </Badge> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] <Badge render={<Link href="/new" />}>New</Badge> ``` </span>Quick Checklist:
asChild → render on ButtonSize Comparison:
coss ui button sizes are more compact compared to shadcn/ui, making them better suited for dense applications. We also introduce new sizes (xs, xl, icon-sm, icon-lg) for more granular control:
| Size | Height (shadcn/ui) | Height (coss ui) |
|---|---|---|
xs | - | 24px |
sm | 32px | 28px |
default | 36px | 32px |
lg | 40px | 36px |
xl | - | 40px |
icon | 36px | 32px |
icon-sm | - | 28px |
icon-lg | - | 36px |
So, for example, if you were using the default size in shadcn/ui and you want to preserve the original height, you should use the lg size in coss ui.
New Variants:
We've added a new destructive-outline variant for better UX patterns:
destructive (solid red) for the main destructive actiondestructive-outline (outline red) to avoid alarming red buttons in the main interfaceComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Button asChild> <Link href="/login">Login</Link> </Button> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] <Button render={<Link href="/login" />}>Login</Button> ``` </span>Quick Checklist:
CardPanel going forward; CardContent remains for legacyQuick Checklist:
asChild → render on CheckboxQuick Checklist:
asChild → render on CollapsibleTriggerCollapsiblePanel; CollapsibleContent remains for legacyComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Collapsible> <CollapsibleTrigger asChild> <Button>Toggle</Button> </CollapsibleTrigger> <CollapsibleContent>Content here</CollapsibleContent> </Collapsible> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] // [!code word:CollapsiblePanel] <Collapsible> <CollapsibleTrigger render={<Button />}>Toggle</CollapsibleTrigger> <CollapsiblePanel>Content here</CollapsiblePanel> </Collapsible> ``` </span>The API is significantly different from shadcn/ui (cmdk). Please review both docs before migrating: cmdk Docs and shadcn/ui Command, and our Base UI Autocomplete docs.
Key Differences:
cmdk dependency - built entirely with Base UI's Autocomplete and Dialog componentsitems array to Command and use render functions instead of manually composing CommandItem childrenCommandCollection within CommandGroup when rendering grouped data with the items patternCommandDialog, CommandDialogTrigger, and CommandDialogPopup for dialog functionality instead of composing separate Dialog componentsCommandGroup uses <CommandGroupLabel> as a child instead of a heading propQuick Checklist:
asChild → render on DialogTrigger and closing buttonsDialogPopup; DialogContent remains for legacyDialogPanel to wrap main content between DialogHeader and DialogFooterasChild on any other parts, switch to the render propComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Dialog> <DialogTrigger asChild> <Button variant="outline">Show Dialog</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Dialog Title</DialogTitle> <DialogDescription>Dialog Description</DialogDescription> </DialogHeader> <DialogFooter> <DialogClose asChild> <Button variant="ghost">Cancel</Button> </DialogClose> </DialogFooter> </DialogContent> </Dialog> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] // [!code word:<DialogPanel>Content</DialogPanel>] <Dialog> <DialogTrigger render={<Button variant="outline" />}> Show Dialog </DialogTrigger> <DialogPopup> <DialogHeader> <DialogTitle>Dialog Title</DialogTitle> <DialogDescription>Dialog Description</DialogDescription> </DialogHeader> <DialogPanel>Content</DialogPanel> <DialogFooter> <DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose> </DialogFooter> </DialogPopup> </Dialog> ``` </span>Quick Checklist:
Group* component names; ButtonGroup* remain for compatibilityGroupSeparator is always required between controls, including outline buttons (unlike shadcn where separators are optional for outline buttons). This ensures consistent focus state handling and better accessibilityasChild on ButtonGroupText, switch to the render prop for custom componentsCompared to shadcn/ui, our Input component includes size variants for better density control. shadcn/ui inputs have a fixed height of 36px, while our component offers flexible sizing with sm (28px), default (32px), and lg (36px) options.
So, if you want to preserve the original shadcn/ui input height (36px), you should use the lg size in coss ui.
Quick Checklist:
InputGroupButton component - use the regular Button component directly inside InputGroupAddon insteadInputGroupInput or InputGroupTextarea directly (and any Button inside it) - no need to add a data-disabled attribute on InputGroup.Prop Mapping:
| Component | Radix UI Prop | Base UI Prop |
|---|---|---|
MenuItem | onSelect | onClick |
Quick Checklist:
asChild → render on MenuTrigger and MenuItemonSelect → onClick on menu items@/components/ui/dropdown-menu → @/components/ui/menuMenu* component names; DropdownMenu* remain for legacyMenuGroupLabel instead of DropdownMenuLabelMenuPopup instead of DropdownMenuContentMenuSubPopup instead of DropdownMenuSubContentasChild on any other parts, switch to the render propComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:onSelect] <DropdownMenu> <DropdownMenuTrigger>Open menu</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem onSelect={() => { console.log("Dashboard") }} > Dashboard </DropdownMenuItem> <DropdownMenuItem>Settings</DropdownMenuItem> <DropdownMenuItem>Sign out</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:onClick] <Menu> <MenuTrigger>Open menu</MenuTrigger> <MenuPopup> <MenuItem onClick={() => { console.log("Dashboard") }} > Dashboard </MenuItem> <MenuItem>Settings</MenuItem> <MenuItem>Sign out</MenuItem> </MenuPopup> </Menu> ``` </span>Quick Checklist:
asChild → render on PopoverTrigger and closing buttonsPopoverPopup; PopoverContent remains for legacyasChild on any other parts, switch to the render propAdditional Notes:
Base UI introduces PopoverTitle and PopoverDescription to structure headings and helper text inside the popup. Base UI also introduces a PopoverClose component for adding close buttons to the popup.
Comparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Popover> <PopoverTrigger asChild> <Button variant="outline">Open Popover</Button> </PopoverTrigger> <PopoverContent> <h2>Popover Title</h2> <p>Popover Description</p> </PopoverContent> </Popover> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:PopoverTitle] // [!code word:PopoverDescription] // [!code word:PopoverClose] // [!code word:render:2] <Popover> <PopoverTrigger render={<Button variant="outline" />}> Open Popover </PopoverTrigger> <PopoverPopup> <PopoverTitle>Popover Title</PopoverTitle> <PopoverDescription>Popover Description</PopoverDescription> <PopoverClose render={<Button variant="ghost" />}>Close</PopoverClose> </PopoverPopup> </Popover> ``` </span>Quick Checklist:
@/components/ui/hover-card → @/components/ui/preview-cardPreviewCard* component names; HoverCard* remain for legacyPreviewCardPopup instead of HoverCardContentasChild on parts, switch to the render propComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <HoverCard> <HoverCardTrigger asChild> <Button variant="outline">Open Preview Card</Button> </HoverCardTrigger> <HoverCardContent>Preview Card Content</HoverCardContent> </HoverCard> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] <PreviewCard> <PreviewCardTrigger render={<Button variant="outline" />}> Open Preview Card </PreviewCardTrigger> <PreviewCardPopup>Preview Card Content</PreviewCardPopup> </PreviewCard> ``` </span>Quick Checklist:
ProgressLabel and ProgressValue for label/value instead of inline elementsProgress, you must include ProgressTrack and ProgressIndicator (otherwise the bar will not display). Without children, a default bar is rendered for youasChild, switch to the render propAdditional Notes:
Base UI introduces separate parts — ProgressLabel, ProgressValue, ProgressTrack, and ProgressIndicator — which you compose inside Progress for greater flexibility.
Quick Checklist:
Radio going forward; RadioGroupItem remains for legacyasChild → render on parts if usedQuick Checklist:
asChild on parts, switch to the render propImportant: Base UI changes how options are provided. Instead of deriving options from children only (Radix pattern), you should pass an items prop (array or record) so values and labels are known before hydration. This avoids SSR pitfalls and improves mount performance. Alternatively, provide a function child to SelectValue to format the label. See the Base UI Select docs.
Prop Mapping:
| Component | Radix UI Prop | Base UI Prop |
|---|---|---|
Select | items | items |
SelectValue | placeholder | removed |
SelectPopup | alignItemWithTrigger | no equivalent |
Quick Checklist:
items prop on Selectplaceholder from SelectSelectPopup instead of SelectContentasChild on parts, switch to the render propSize Comparison:
coss ui select sizes are more compact compared to shadcn/ui, making them better suited for dense applications:
| Size | Height (shadcn/ui) | Height (coss ui) |
|---|---|---|
sm | 32px | 28px |
default | 36px | 32px |
lg | - | 36px |
So, for example, if you were using the default size in shadcn/ui and you want to preserve the original height, you should use the lg size in coss ui.
Additional Notes:
Base UI introduces the alignItemWithTrigger prop to control whether the SelectContent overlaps the SelectTrigger so the selected item's text is aligned with the trigger's value text.
Comparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:placeholder="Select a framework"] <Select> <SelectTrigger> <SelectValue placeholder="Select a framework" /> </SelectTrigger> <SelectContent> <SelectItem value="next">Next.js</SelectItem> <SelectItem value="vite">Vite</SelectItem> <SelectItem value="astro">Astro</SelectItem> </SelectContent> </Select> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:alignItemWithTrigger={false}] // [!code word:items:2] <Select items={[ { label: "Select a framework", value: null }, { label: "Next.js", value: "next" }, { label: "Vite", value: "vite" }, { label: "Astro", value: "astro" }, ]} > <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectPopup alignItemWithTrigger={false}> {items.map((item) => ( <SelectItem key={item.value} value={item}> {item.label} </SelectItem> ))} </SelectPopup> </Select> ``` </span>Quick Checklist:
asChild → render on SheetTrigger and closing buttonsSheetPopup; SheetContent remains for legacySheetPanel to wrap main contentasChild on any other parts, switch to the render propComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Sheet> <SheetTrigger asChild> <Button variant="outline">Open Sheet</Button> </SheetTrigger> <SheetContent> <SheetHeader> <SheetTitle>Sheet Title</SheetTitle> </SheetHeader> Content here <SheetFooter> <SheetClose asChild> <Button>Close</Button> </SheetClose> </SheetFooter> </SheetContent> </Sheet> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] // [!code word:<SheetPanel>Content here</SheetPanel>] <Sheet> <SheetTrigger render={<Button variant="outline" />}> Open Sheet </SheetTrigger> <SheetPopup> <SheetHeader> <SheetTitle>Sheet Title</SheetTitle> </SheetHeader> <SheetPanel>Content here</SheetPanel> <SheetFooter> <SheetClose render={<Button />}>Close</SheetClose> </SheetFooter> </SheetPopup> </Sheet> ``` </span>Quick Checklist:
Slider uses Base UI's multiple value approachvalue={[50]} instead of value={50})onValueChange receives an array of numbersasChild → render on parts if usedQuick Checklist:
asChild → render on Switch if usedQuick Checklist:
asChild → render on parts if usedTabsTab going forward; TabsTrigger remains for legacyTabsPanel; TabsContent remains for legacyAdditional Notes:
Compared to shadcn/ui, our TabsList component adds variant prop, which allows you to choose between default and underline styles.
Comparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:TabsContent] <Tabs defaultValue="tab-1"> <TabsList> <TabsTab value="tab-1">Tab 1</TabsTab> <TabsTab value="tab-2">Tab 2</TabsTab> <TabsTab value="tab-3">Tab 3</TabsTab> </TabsList> <TabsContent value="tab-1">Tab 1 content</TabsContent> <TabsContent value="tab-2">Tab 2 content</TabsContent> <TabsContent value="tab-3">Tab 3 content</TabsContent> </Tabs> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:TabsPanel] <Tabs defaultValue="tab-1"> <TabsList> <TabsTab value="tab-1">Tab 1</TabsTab> <TabsTab value="tab-2">Tab 2</TabsTab> <TabsTab value="tab-3">Tab 3</TabsTab> </TabsList> <TabsPanel value="tab-1">Tab 1 content</TabsPanel> <TabsPanel value="tab-2">Tab 2 content</TabsPanel> <TabsPanel value="tab-3">Tab 3 content</TabsPanel> </Tabs> ``` </span>Compared to shadcn/ui, our Textarea component includes size variants (sm, default, lg) for better density control. For visual consistency, if you're using size="lg" on other form elements like inputs, you should add the same size to textareas as well.
The API is significantly different from shadcn/ui (Sonner). Please review both docs before migrating: Sonner Docs and shadcn/ui Sonner, and our Base UI toast docs.
Quick Checklist:
<Toaster /> component in layout → <ToastProvider> wrapperComparison Examples:
shadcn/ui (Sonner)
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
</body>
</html>
)
}
toast("Event has been created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
cancel: {
label: "Undo",
},
})
coss ui (Base UI)
import { ToastProvider } from "@/components/ui/toast"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ToastProvider>
<main>{children}</main>
</ToastProvider>
</body>
</html>
)
}
onClick={() => {
const id = toastManager.add({
title: "Event has been created",
description: "Sunday, December 03, 2023 at 9:00 AM",
type: "success",
actionProps: {
children: "Undo",
onClick: () => toastManager.close(id),
},
})
}}
Quick Checklist:
asChild → render on Toggle if usedProp Mapping:
| Component | Radix UI Prop | Base UI Prop |
|---|---|---|
ToggleGroup | type (enum, "single" or "multiple") | multiple (boolean, default: false) |
Quick Checklist:
type="multiple" → multiple on ToggleGrouptype="single" from ToggleGroupdefaultValueToggle going forward; ToggleGroupItem remains for legacyasChild → render on parts if usedSize Comparison:
coss ui toggle group sizes are more compact compared to shadcn/ui, making them better suited for dense applications:
| Size | Height (shadcn/ui) | Height (coss ui) |
|---|---|---|
sm | 32px | 28px |
default | 36px | 32px |
lg | - | 36px |
So, for example, if you were using the default size in shadcn/ui and you want to preserve the original height, you should use the lg size in coss ui.
Comparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:type="multiple"] <ToggleGroup type="multiple" defaultValue={["bold"]}> <ToggleGroupItem value="bold">B</ToggleGroupItem> <ToggleGroupItem value="italic">I</ToggleGroupItem> <ToggleGroupItem value="underline">U</ToggleGroupItem> </ToggleGroup> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:multiple] <ToggleGroup multiple defaultValue={["bold"]}> <Toggle value="bold">B</Toggle> <Toggle value="italic">I</Toggle> <Toggle value="underline">U</Toggle> </ToggleGroup> ``` </span>Quick Checklist:
asChild → render on TooltipTriggerTooltipPopup; TooltipContent remains for legacyComparison Example:
<span data-lib="radix-ui"> ```tsx title="shadcn/ui" // [!code word:asChild] <Tooltip> <TooltipTrigger asChild> <Button variant="outline">Hover me</Button> </TooltipTrigger> <TooltipContent>Tooltip content</TooltipContent> </Tooltip> ``` </span> <span data-lib="base-ui"> ```tsx title="coss ui" // [!code word:render] <Tooltip> <TooltipTrigger render={<Button variant="outline" />}> Hover me </TooltipTrigger> <TooltipPopup>Tooltip content</TooltipPopup> </Tooltip> ``` </span>If you encounter issues during migration or have questions about specific components, please: