Files
calendar/packages/coss-ui/radix_shadcn_migration_guide.md
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
25fa46fd34 fix(coss-ui): convert /settings/my-account/general to coss-ui (#26053)
* fix(coss-ui): convert /settings/my-account/general to coss-ui

* update guide

* fix: add menuPosition fixed to TimezoneSelect in TravelScheduleModal

Co-Authored-By: [email protected] <[email protected]>

* fix: use menuPortalTarget to render TimezoneSelect dropdown above dialog footer

Co-Authored-By: [email protected] <[email protected]>

* fix: use Object.assign for type-safe styles in TimezoneSelect

Co-Authored-By: [email protected] <[email protected]>

* revert comment

* fix: guard document access for SSR in TravelScheduleModal

Addresses Cubic AI review feedback: menuPortalTarget={document.body} will
throw during server rendering where document is undefined. Added typeof
check to guard against SSR.

Co-Authored-By: unknown <>

* change dialog backdrop from blur to dim

* fix: add menuPlacement auto to open dropdown upward on mobile

Co-Authored-By: [email protected] <[email protected]>

* fix: use menuPlacement top on mobile for TimezoneSelect dropdown

Co-Authored-By: [email protected] <[email protected]>

* fix: make timezone button stack vertically on mobile to prevent overflow

Co-Authored-By: [email protected] <[email protected]>

* fix: make timezone select and button 50/50 width on larger viewports

Co-Authored-By: [email protected] <[email protected]>

* fix date range picker issue on mobile

* prevent travel schedule dialog dismissal with date picker

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-23 11:19:00 +00:00

28 KiB

title, description
title description
Radix / shadcn Migration A comprehensive guide for migrating from Radix UI and shadcn/ui to coss ui components.

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.

Overview

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:

  • Providing detailed component-by-component migration guides below
  • Maintaining similar component names and structures where it makes sense
  • Offering clear prop mappings and code examples

General Migration Patterns

The asChild to render Pattern

The most common change across all components is replacing Radix UI's asChild prop with Base UI's render prop:

```tsx title="Radix / shadcn" // [!code word:asChild] Edit profile ``` ```tsx title="coss ui" // [!code word:render] }>Edit profile ```

Component Naming Conventions

Some components have updated names for clarity and consistency:

  • *Content → *Popup or *Panel (e.g., DialogContent → DialogPopup)
  • Legacy names are often kept for backward compatibility

Component Migration Guides

Accordion

Quick Checklist:

  • Replace type="multiple" → multiple={true} on Accordion
  • Remove type="single" from Accordion
  • Remove collapsible from Accordion
  • Always use arrays for defaultValue
  • Use AccordionPanel going forward; AccordionContent remains for legacy
  • If you used asChild on parts, switch to the render prop

Prop Mapping:

Component Radix UI Prop Base UI Prop
Accordion type (enum, "single" or "multiple") multiple (boolean, default: false)
Accordion collapsible removed

Comparison Example:

```tsx title="shadcn/ui" // [!code word:type="multiple"] // [!code word:collapsible] // [!code word:"item-1":1] Title Content ``` ```tsx title="coss ui" // [!code word:multiple={true}] // [!code word:\{\["item-1"\]\}] Title Content ```

Alert

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-foreground

Alert Dialog

Quick Checklist:

  • Replace asChild → render on AlertDialogTrigger and closing buttons
  • Replace AlertDialogAction and AlertDialogCancel → AlertDialogClose
  • Prefer AlertDialogPopup; AlertDialogContent remains for legacy
  • Use AlertDialogPanel to wrap main content between AlertDialogHeader and AlertDialogFooter
  • If you used asChild on any other parts, switch to the render prop

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] // [!code word:AlertDialogCancel] // [!code word:AlertDialogAction] Show Alert Dialog Are you absolutely sure? This action cannot be undone. Cancel Continue ``` ```tsx title="coss ui" // [!code word:render] // [!code word:AlertDialogClose] // [!code word:Content] }> Show Alert Dialog Are you absolutely sure? This action cannot be undone. Content }> Cancel }> Continue ```

Avatar

Quick Checklist:

  • Replace asChild → render on Avatar

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] U ``` ```tsx title="coss ui" // [!code word:render] }> U ```

Badge

Quick Checklist:

  • Replace asChild → render on Badge

Size 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-foreground

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] New ``` ```tsx title="coss ui" // [!code word:render] }>New ```

Button

Quick Checklist:

  • Replace asChild → render on Button

Size 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:

  • Primary actions: Use destructive (solid red) for the main destructive action
  • Secondary triggers: Use destructive-outline (outline red) to avoid alarming red buttons in the main interface

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Login ``` ```tsx title="coss ui" // [!code word:render] }>Login ```

Card

Quick Checklist:

  • Use CardPanel going forward; CardContent remains for legacy

Checkbox

Quick Checklist:

  • Replace asChild → render on Checkbox

Collapsible

Quick Checklist:

  • Replace asChild → render on CollapsibleTrigger
  • Prefer CollapsiblePanel; CollapsibleContent remains for legacy

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Toggle Content here ``` ```tsx title="coss ui" // [!code word:render] // [!code word:CollapsiblePanel] }>Toggle Content here ```

Command

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:

  • No cmdk dependency - built entirely with Base UI's Autocomplete and Dialog components
  • Data-driven approach - pass an items array to Command and use render functions instead of manually composing CommandItem children
  • Use CommandCollection within CommandGroup when rendering grouped data with the items pattern
  • Use CommandDialog, CommandDialogTrigger, and CommandDialogPopup for dialog functionality instead of composing separate Dialog components
  • CommandGroup uses <CommandGroupLabel> as a child instead of a heading prop

Dialog

Quick Checklist:

  • Replace asChild → render on DialogTrigger and closing buttons
  • Prefer DialogPopup; DialogContent remains for legacy
  • Use DialogPanel to wrap main content between DialogHeader and DialogFooter
  • If you used asChild on any other parts, switch to the render prop

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Show Dialog Dialog Title Dialog Description Cancel ``` ```tsx title="coss ui" // [!code word:render] // [!code word:Content] }> Show Dialog Dialog Title Dialog Description Content }>Cancel ```

Group (Button Group)

Quick Checklist:

  • Prefer Group* component names; ButtonGroup* remain for compatibility
  • GroupSeparator 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 accessibility
  • If you used asChild on ButtonGroupText, switch to the render prop for custom components

Input

Compared 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.

Input Group

Quick Checklist:

  • No InputGroupButton component - use the regular Button component directly inside InputGroupAddon instead
  • To disable an input group, disable the InputGroupInput or InputGroupTextarea directly (and any Button inside it) - no need to add a data-disabled attribute on InputGroup.

Menu

Prop Mapping:

Component Radix UI Prop Base UI Prop
MenuItem onSelect onClick

Quick Checklist:

  • Replace asChild → render on MenuTrigger and MenuItem
  • Replace onSelect → onClick on menu items
  • Update import path from @/components/ui/dropdown-menu → @/components/ui/menu
  • Prefer Menu* component names; DropdownMenu* remain for legacy
  • Prefer MenuGroupLabel instead of DropdownMenuLabel
  • Prefer MenuPopup instead of DropdownMenuContent
  • Prefer MenuSubPopup instead of DropdownMenuSubContent
  • If you used asChild on any other parts, switch to the render prop

Comparison Example:

```tsx title="shadcn/ui" // [!code word:onSelect] Open menu { console.log("Dashboard") }} > Dashboard Settings Sign out ``` ```tsx title="coss ui" // [!code word:onClick] Open menu { console.log("Dashboard") }} > Dashboard Settings Sign out ```

Popover

Quick Checklist:

  • Replace asChild → render on PopoverTrigger and closing buttons
  • Prefer PopoverPopup; PopoverContent remains for legacy
  • If you used asChild on any other parts, switch to the render prop

Additional 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:

```tsx title="shadcn/ui" // [!code word:asChild] Open Popover

Popover Title

Popover Description

```
```tsx title="coss ui" // [!code word:PopoverTitle] // [!code word:PopoverDescription] // [!code word:PopoverClose] // [!code word:render:2] }> Open Popover Popover Title Popover Description }>Close ```

Preview Card

Quick Checklist:

  • Update import path from @/components/ui/hover-card → @/components/ui/preview-card
  • Prefer PreviewCard* component names; HoverCard* remain for legacy
  • Prefer PreviewCardPopup instead of HoverCardContent
  • If you used asChild on parts, switch to the render prop

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Open Preview Card Preview Card Content ``` ```tsx title="coss ui" // [!code word:render] }> Open Preview Card Preview Card Content ```

Progress

Quick Checklist:

  • Prefer ProgressLabel and ProgressValue for label/value instead of inline elements
  • If you render children inside Progress, you must include ProgressTrack and ProgressIndicator (otherwise the bar will not display). Without children, a default bar is rendered for you
  • If you used asChild, switch to the render prop

Additional Notes:

Base UI introduces separate parts — ProgressLabel, ProgressValue, ProgressTrack, and ProgressIndicator — which you compose inside Progress for greater flexibility.

Radio Group

Quick Checklist:

  • Use Radio going forward; RadioGroupItem remains for legacy
  • Replace asChild → render on parts if used

Scroll Area

Quick Checklist:

  • If you used asChild on parts, switch to the render prop

Select

Important: 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:

  • Set items prop on Select
  • Remove placeholder from Select
  • Prefer SelectPopup instead of SelectContent
  • If you used asChild on parts, switch to the render prop

Size 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:

```tsx title="shadcn/ui" // [!code word:placeholder="Select a framework"] Next.js Vite Astro ``` ```tsx title="coss ui" // [!code word:alignItemWithTrigger={false}] // [!code word:items:2] {items.map((item) => ( {item.label} ))} ```

Sheet

Quick Checklist:

  • Replace asChild → render on SheetTrigger and closing buttons
  • Prefer SheetPopup; SheetContent remains for legacy
  • Use SheetPanel to wrap main content
  • If you used asChild on any other parts, switch to the render prop

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Open Sheet Sheet Title Content here Close ``` ```tsx title="coss ui" // [!code word:render] // [!code word:Content here] }> Open Sheet Sheet Title Content here }>Close ```

Slider

Quick Checklist:

  • coss ui Slider uses Base UI's multiple value approach
  • Always pass values as arrays (e.g., value={[50]} instead of value={50})
  • onValueChange receives an array of numbers
  • Multiple thumbs are supported natively via array length
  • Replace asChild → render on parts if used

Switch

Quick Checklist:

  • Replace asChild → render on Switch if used

Tabs

Quick Checklist:

  • Replace asChild → render on parts if used
  • Use TabsTab going forward; TabsTrigger remains for legacy
  • Prefer TabsPanel; TabsContent remains for legacy

Additional Notes:

Compared to shadcn/ui, our TabsList component adds variant prop, which allows you to choose between default and underline styles.

Comparison Example:

```tsx title="shadcn/ui" // [!code word:TabsContent] Tab 1 Tab 2 Tab 3 Tab 1 content Tab 2 content Tab 3 content ``` ```tsx title="coss ui" // [!code word:TabsPanel] Tab 1 Tab 2 Tab 3 Tab 1 content Tab 2 content Tab 3 content ```

Textarea

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.

Toast

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:

  • Replace <Toaster /> component in layout → <ToastProvider> wrapper
  • Toast API calls differ significantly - see comparison below
  • Toast actions use different patterns

Comparison 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),
    },
  })
}}

Toggle

Quick Checklist:

  • Replace asChild → render on Toggle if used

Toggle Group

Prop Mapping:

Component Radix UI Prop Base UI Prop
ToggleGroup type (enum, "single" or "multiple") multiple (boolean, default: false)

Quick Checklist:

  • Replace type="multiple" → multiple on ToggleGroup
  • Remove type="single" from ToggleGroup
  • Always use arrays for defaultValue
  • Use Toggle going forward; ToggleGroupItem remains for legacy
  • Replace asChild → render on parts if used

Size 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:

```tsx title="shadcn/ui" // [!code word:type="multiple"] B I U ``` ```tsx title="coss ui" // [!code word:multiple] B I U ```

Tooltip

Quick Checklist:

  • Replace asChild → render on TooltipTrigger
  • Prefer TooltipPopup; TooltipContent remains for legacy

Comparison Example:

```tsx title="shadcn/ui" // [!code word:asChild] Hover me Tooltip content ``` ```tsx title="coss ui" // [!code word:render] }> Hover me Tooltip content ```

Additional Resources

Need Help?

If you encounter issues during migration or have questions about specific components, please:

  • Check the individual component documentation pages
  • Review the Base UI documentation for deeper understanding
  • Open an issue on our GitHub repository