Skip to content

Migrate UI component library from React Aria to ShadCN 2.0 with BaseUI (breaking change)#836

Open
tjementum wants to merge 108 commits intomainfrom
migrate-to-shadcn-2.0-and-base-ui
Open

Migrate UI component library from React Aria to ShadCN 2.0 with BaseUI (breaking change)#836
tjementum wants to merge 108 commits intomainfrom
migrate-to-shadcn-2.0-and-base-ui

Conversation

@tjementum
Copy link
Member

@tjementum tjementum commented Jan 23, 2026

Summary & Motivation

Replace the React Aria Components-based UI library with ShadCN 2.0 built on BaseUI primitives.

Why ShadCN:

  • Community and adoption: ShadCN has become the dominant component library for new React projects, with a significantly larger community than React Aria Components
  • BaseUI is the future: ShadCN 2.0 replaces Radix UI with BaseUI (@base-ui/react) as its headless primitive layer, making this the right time to adopt the new foundation
  • Rich ecosystem: Beyond basic form controls, ShadCN offers a large catalog of ready-to-use components and blocks -- charts (area, bar, line, pie, radar, radial), dashboards, sidebars, navigation menus, drawers, sheets, carousels, resizable panels, command palettes, data tables with sorting/filtering, and pre-built page blocks for login, signup, OTP verification, calendars with time pickers, and more
  • On distribution for AI: ShadCN is heavily represented in AI training data, making it "on distribution" for models like Claude and GPT -- similar to how Python is on distribution while COBOL is not. AI tools like Claude Code produce significantly better UI code when targeting ShadCN patterns, generating correct and visually polished components on the first attempt

Component migration:

  • Migrate 30+ components (Button, Input, Select, Dialog, Menu, Table, Calendar, etc.) from React Aria to ShadCN 2.0 with BaseUI headless primitives
  • Replace tailwind-variants with class-variance-authority (cva) and remove react-aria-components and tailwindcss-react-aria-components dependencies
  • Replace custom Toast system with Sonner and custom ThemeMode provider with next-themes, adding a dark mode flash-prevention script in index.html
  • Replace custom OneTimeCodeInput with input-otp library and custom Calendar/DatePicker with react-day-picker
  • Replace custom SearchField and NumberField with InputGroup-based composition
  • Replace custom Image component with native <img> and custom Heading/Text with native HTML elements using global Tailwind styles
  • Add new ShadCN components: Alert, Card, ContextMenu (with long-press for mobile), Empty, Skeleton, ScrollArea, SidePane, TablePagination, Tabs, and collapsible Breadcrumb
  • Remove ReactAriaRouterProvider and simplify the root component tree
  • Delete 45 unused React Aria components (Accordion, ComboBox, GridList, Meter, MultiSelect, Slider, Switch, etc.)
  • Update E2E tests for the new component selectors and stabilize Firefox tests using dispatchEvent for dropdown interactions

Rem-based scaling system:

All sizing throughout the UI has been converted from px to rem, making it possible to scale the entire web application by changing a single CSS variable (--zoom-level) on the root element. This is essential for embedding the web app inside a native mobile app where the webview needs to render at a different scale than the browser default. The zoom level is persisted in localStorage and applied before first paint to avoid layout shifts. The side menu, app layout, control heights, spacing, border radius, and all arbitrary Tailwind values use rem units so everything scales proportionally.

Accessibility and mobile-friendly customizations:

Stock ShadCN components have been customized to be more accessible and mobile-friendly:

  • All interactive controls (buttons, inputs, selects, menu items, table rows, checkboxes, radio buttons, toggles) use a 44px default height via the --control-height CSS variable, meeting Apple Human Interface Guidelines for touch targets -- while still looking good on desktop
  • Consistent focus ring styling across all components using outline-ring focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 instead of ShadCN's default ring utilities
  • cursor-pointer on all clickable elements and active: press feedback (background color change) on buttons, menu items, and input fields for tactile mobile touch response
  • Tooltips work with click/tap on mobile instead of hover-only
  • ContextMenu supports long-press gesture on touch devices
  • Calendar and DateRangePicker are locale-aware, using the app locale for day names, month names, and first day of week
  • Date formatting uses the browser Intl API for locale-aware display throughout the app
  • Dialog uses DialogBody for proper mobile full-screen scrolling, Select renders below the trigger with larger touch targets, DropdownMenu uses auto width, Button destructive variant uses a solid background for WCAG contrast, and AlertDialog supports vertically centered icons
  • Buttons render full-width on mobile by default
  • Dialog footer buttons have consistent height and full-width mobile styling
  • Localized aria-labels on legal and footer pages for better screen reader support

UI polish and improvements:

  • Migrate color theme from HSL to OKLCH color space with a macOS-inspired neutral dark mode
  • Empty state component with icon, title, and description for tables and lists
  • Skeleton loading states replacing spinners throughout the app
  • Card component used for dashboard, sessions, and legal pages
  • Collapsible breadcrumbs that progressively collapse middle items when space is tight
  • Sticky toolbar with scroll-away header on mobile for more content space
  • Standardized heading elements with global Tailwind styles and consistent spacing to separators
  • Standardized icon sizing using size-N utility and replaced deprecated social media icons
  • Standardized z-index layering using Tailwind defaults and semantic layers
  • Arrow key navigation with roving tabindex in Table, and auto-scroll to selected row for deep link navigation
  • Keyboard-accessible table column headers and side pane close button
  • Removed generic success and HTTP status code titles from toasts and increased toast duration
  • Improved TenantSelector logo animation during sidebar collapse, support dialog mobile menu integration, and keyboard navigation order with skip link priority
  • Eliminated duplicate user list API requests by isolating desktop and mobile query paths
  • Simplified UserQuerying and UserToolbar with ResizeObserver for adaptive inline filters

Infrastructure and tooling:

  • Add @rsbuild/plugin-source-build with proper source field exports for shared-webapp monorepo packages, enabling hot reload when editing shared components
  • Add ShadCN MCP server configuration for component installation
  • Fix developer CLI Node version detection to check version manager install paths (fnm, nvm, volta) instead of relying on PATH
  • Update frontend rule files documenting ShadCN 2.0 with BaseUI patterns, Apple HIG compliance, rem-based sizing, and dialog conventions

Downstream projects

This is a breaking change. The entire UI component library has been replaced, affecting every component import and prop API across self-contained systems. This is a significant migration effort.

Important: The migration steps below are incomplete (and AI-generated). They cover the most critical and structural changes, but there will be additional compile errors, prop API changes, and component replacements not listed here. Additionally, more breaking changes are planned, so be aware that this is just the first of many big breaking changes to PlatformPlatform, and the upgrade guide below is a best effort.

Recommended migration approach:

  1. Create a new branch in the downstream project
  2. Cherry-pick each commit from this branch one by one, starting from the first
  3. The first few commits are meta/configuration changes (ShadCN setup, rule updates, dependency changes) -- apply these first
  4. After that, each commit migrates a single component (Button, Input, Label, etc.). For each component commit:
    • Cherry-pick the commit
    • Fix all compile errors in the downstream self-contained system caused by the new component API
    • Update any E2E tests that reference the migrated component
    • Build, run all tests, and run all E2E tests -- everything must pass before moving to the next component
  5. Continue one component at a time until all ~28 component migration commits are applied
  6. The later commits (after "Remove remaining React Aria Component dependencies") are visual polish, accessibility improvements, and customizations -- these can be applied more freely

The recommended approach is to use Claude Code with the /ralph-loop command for each component migration step. The loop will iteratively fix compile errors, update tests, and verify E2E tests pass until the system is fully working with the new component.

Key breaking changes (listed in commit order):

Each component migration commit changes the shared component API. For each one, fix all compile errors and E2E test failures in the downstream self-contained system before moving to the next commit. Note that this list is not exhaustive -- additional breaking changes will surface during migration that are not documented here.

  1. "Migrate Button component from React Aria to ShadCN 2.0" (commit 4): onPress becomes onClick throughout downstream code.

  2. Each component commit (commits 4-29): As each component is migrated, its React Aria props change to standard HTML/ShadCN equivalents -- isDisabled becomes disabled, isRequired becomes required, etc. Fix usages of that specific component in the downstream self-contained system before moving to the next commit.

  3. "Migrate Dialog and Modal components from React Aria to ShadCN 2.0" (commit 13): Replace DirtyModal with DirtyDialog (from @repo/ui/components/DirtyDialog). Dialog now uses <DialogBody> between header and footer, and <DialogClose render={<Button type="reset" />}> for cancel buttons.

  4. "Migrate Toast component from React Aria to Sonner and replace custom Theme with next-themes" (commit 19): This commit requires three changes:

    Update your-self-contained-system/WebApp/bootstrap.tsx:

    -import { GlobalToastRegion } from "@repo/ui/components/Toast";
    +import { Toaster } from "@repo/ui/components/Sonner";
    -        <GlobalToastRegion />
    +        <Toaster position="top-center" closeButton={true} style={{ zIndex: 60 }} />

    Replace useToast / toast.add(...) with Sonner's toast.success(...) / toast.error(...) throughout the self-contained system.

    Add dark mode flash-prevention script to your-self-contained-system/WebApp/public/index.html after the existing nonce script:

    <script nonce="{{cspNonce}}">
      (()=> {var t=localStorage.getItem('theme'),d=window.matchMedia('(prefers-color-scheme:dark)').matches;if(t==='dark'||(!t&&d)) { document.documentElement.classList.add('dark') }})();
    </script>
  5. "Migrate Heading and Text components from React Aria to native HTML elements" (commit 24): Replace <Heading> and <Text> with native <h1>-<h4> and <p> elements.

  6. "Remove remaining React Aria Component dependencies and complete ShadCN 2.0 migration" (commit 31): Update your-self-contained-system/WebApp/routes/__root.tsx -- remove the <ReactAriaRouterProvider> wrapper from the component tree (keep the children as-is):

    -import { ReactAriaRouterProvider } from "@repo/infrastructure/router/ReactAriaRouterProvider";
  7. "Remove custom Image component in favor of native img with localized alt text" (commit 43): Replace <Image> with native <img> with localized alt text.

  8. "Enable hot reload for shared-webapp monorepo packages with source build plugin and proper import conventions" (commit 47): Update your-self-contained-system/WebApp/rsbuild.config.ts to add the source build plugin:

    +import { pluginSourceBuild } from "@rsbuild/plugin-source-build";

    Add to rspack config:

         watchOptions: {
    -        ignored: ["**/tests/**", "**/playwright-report/**"]
    +        ignored: ["**/tests/**", "**/playwright-report/**"],
    +        followSymlinks: true
    +      },
    +      snapshot: {
    +        managedPaths: []
         }

    Add the plugin:

    +    pluginSourceBuild({
    +      sourceField: "source"
    +    }),
  9. Zoom level script: Add to your-self-contained-system/WebApp/public/index.html after the dark mode script:

    <script nonce="{{cspNonce}}">
      (()=> {var zoomLevel=localStorage.getItem('zoom-level');if(zoomLevel) { document.documentElement.style.setProperty('--zoom-level',zoomLevel) }})();
    </script>
  10. Custom components: Any custom components built on React Aria primitives need to be rebuilt using ShadCN 2.0 patterns. This includes replacing react-aria-components imports with BaseUI/ShadCN equivalents, replacing tailwind-variants (tv()) with class-variance-authority (cva()), and replacing focusRing utility with outline-ring focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2. Look at the migrated components in shared-webapp/ui/components as reference for the new patterns.

Checklist

  • I have added tests, or done manual regression tests
  • I have updated the documentation, if necessary

@tjementum tjementum self-assigned this Jan 23, 2026
@tjementum tjementum requested a review from a team as a code owner January 23, 2026 17:48
@tjementum tjementum added the Enhancement New feature or request label Jan 23, 2026
@tjementum tjementum force-pushed the migrate-to-shadcn-2.0-and-base-ui branch 3 times, most recently from f025236 to 57d739a Compare January 23, 2026 22:07
@tjementum tjementum force-pushed the migrate-to-shadcn-2.0-and-base-ui branch from 57d739a to c0a0c79 Compare January 28, 2026 14:49
@tjementum tjementum moved this to 🏗 In Progress in Kanban board Jan 28, 2026
@tjementum tjementum closed this Jan 28, 2026
@tjementum tjementum deleted the migrate-to-shadcn-2.0-and-base-ui branch January 28, 2026 15:56
@github-project-automation github-project-automation bot moved this from 🏗 In Progress to ✅ Done in Kanban board Jan 28, 2026
@tjementum tjementum force-pushed the migrate-to-shadcn-2.0-and-base-ui branch from d8c61ba to 9eedc68 Compare February 7, 2026 17:36
@tjementum tjementum force-pushed the migrate-to-shadcn-2.0-and-base-ui branch from 9eedc68 to 93240e4 Compare February 7, 2026 19:50
@tjementum tjementum added the Deploy to Staging Set this label on pull requests to deploy code or infrastructure to the Staging environment label Feb 8, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 8, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Deploy to Staging Set this label on pull requests to deploy code or infrastructure to the Staging environment Enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant