ADR 011: shadcn/ui as Component Foundation

AcceptedDate: 2026-03-03

Context

The A2R design system currently provides only four custom components (Button, Modal, ThemeProvider, Typography) built with CSS Modules. Building accessible, production-ready components from scratch is expensive: each component requires WAI-ARIA compliance, keyboard navigation, focus management, screen-reader testing, and cross-browser validation. At the current pace, delivering the 60+ components the platform needs (dialogs, dropdowns, tooltips, tabs, command palettes, date pickers, etc.) would take years.

Meanwhile, the front-end ecosystem has shifted significantly:

  • Radix UI is in de facto wind-down. The original co-creator (Colm Tuite) publicly called the project "a liability" and moved on to co-found Base UI at MUI. Only one maintainer (Chance Strickland) continues to make sporadic commits. The repository has 774 open issues, and the last substantive release was November 2025. There is no public roadmap or commitment to long-term maintenance.
  • Base UI emerged as the successor. Created by the original Radix authors at MUI, @base-ui/react reached v1.0 in December 2025. It carries forward the headless, unstyled primitives philosophy of Radix but with active corporate backing, a dedicated team, and a clear roadmap. It supports React 19 and ships WAI-ARIA compliant components out of the box.
  • shadcn/ui added Base UI support in December 2025, providing a migration path away from Radix primitives. Projects can now choose between Radix and Base UI as the underlying primitive layer.
  • The existing stack is fully compatible. React 19, TypeScript 5, Tailwind CSS v4, and Rsbuild all work with shadcn/ui and Base UI without additional configuration.

Decision

Adopt shadcn/ui with Base UI (@base-ui/react ^1.2.0) as the component foundation for the A2R design system. Visual style: base-lyra.

shadcn/ui + Base UI as Component Foundation shadcn/ui + Base UI as Component Foundation Positive 60+ accessible components, owned source code Base UI actively maintained (MUI backing) Full Tailwind CSS v4 + OKLCH color alignment React 19 compatible, RTL support, 5 visual styles Negative Base UI ecosystem less mature than Radix render prop instead of asChild (different pattern) Manual Rsbuild setup (CLI doesn't auto-detect) New dependencies + token mapping effort

Not a dependency -- owned source code

shadcn/ui is not a component library installed as an npm package. Instead, components are copied into the project as source code in src/components/ui/. The team owns every line, can modify any component freely, and is never blocked by upstream release cycles. Updates are opt-in: the CLI can diff changes, but nothing is pulled automatically.

Base UI for accessibility

@base-ui/react (^1.2.0) provides the headless, unstyled primitive layer that handles:

  • WAI-ARIA roles, states, and properties
  • Keyboard navigation and focus management
  • Screen-reader announcements
  • Pointer and touch interaction
  • Dismissal logic (Escape key, outside click)

Components built on Base UI inherit these behaviors without the design system team needing to re-implement accessibility patterns for every new component.

Why Base UI over Radix

FactorRadix UIBase UI
Active maintainers1 (sporadic)Dedicated team at MUI
Corporate backingNone (Modulz dissolved)MUI (profitable, established)
Last releaseNovember 2025Active monthly releases
Open issues774+Actively triaged
React 19 supportPartialFull
Creator recommendationCo-creator moved to Base UICreated by original Radix authors
Composition patternasChild proprender prop
shadcn/ui supportYes (original default)Yes (since December 2025)

The original Radix co-creator (Colm Tuite) publicly recommended Base UI as the forward-looking choice. Choosing Base UI avoids building on a project with uncertain maintenance while gaining access to the same design philosophy from the same creators.

Tailwind CSS v4 integration

shadcn/ui uses Tailwind CSS for all component styling. This aligns with the platform's existing Tailwind setup:

  • OKLCH colors: Tailwind v4 uses the OKLCH color space by default, which provides perceptually uniform color manipulation. shadcn/ui's token system maps cleanly to OKLCH values.
  • CSS-first configuration: Tailwind v4's @theme directive replaces the old tailwind.config.js, and shadcn/ui's variables integrate directly into CSS.
  • Full Tailwind in the design system: The design system package uses Tailwind utility classes exclusively for component styling. CSS Module files (.module.css) are removed from the design system. MFE teams remain free to choose their own styling approach (CSS Modules, Tailwind, or both).

Visual style: base-lyra

shadcn/ui offers multiple visual styles. base-lyra is chosen for A2R because:

  • Boxy, sharp edges: No rounded corners on most elements, creating a professional, structured appearance.
  • Serif display compatibility: Pairs naturally with A2R's Faculty Glyphic serif headings.
  • Professional brand alignment: The geometric, editorial aesthetic fits A2R's brand identity.

Integration with dual distribution

The existing dual distribution model (tsup for npm types/tokens + Rsbuild for MF remote) remains unchanged. shadcn/ui components are exposed via Module Federation just like the existing custom components:

// rsbuild.config.ts exposes map (excerpt)
exposes: {
  "./Button": "./src/components/ui/button.tsx",
  "./Dialog": "./src/components/ui/dialog.tsx",
  "./Dropdown": "./src/components/ui/dropdown-menu.tsx",
  // ... additional shadcn components
  "./ThemeProvider": "./src/components/ThemeProvider/index.ts",
  "./hooks": "./src/hooks/index.ts",
  "./utils": "./src/utils/index.ts",
}

components.json for Rsbuild

shadcn/ui requires a components.json configuration file at the package root. Because the CLI does not auto-detect Rsbuild, manual setup is required:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "base-lyra",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

Key configuration choices:

  • rsc: false: The design system is CSR-only (no React Server Components).
  • style: "base-lyra": Boxy, sharp edges visual style.
  • cssVariables: true: Uses CSS custom properties for theming, integrating with the existing token system.

Token namespace mapping

shadcn/ui defines its own CSS custom property namespace. These must be mapped to the existing --ds-* design system tokens to maintain consistency:

shadcn variableMaps toPurpose
--background--ds-bg-primaryPage background
--foreground--ds-text-primaryDefault text color
--primary--ds-color-primary-600Primary brand color
--primary-foreground#FFFFFFText on primary
--secondary--ds-bg-secondarySecondary backgrounds
--secondary-foreground--ds-text-primaryText on secondary
--muted--ds-color-neutral-100Muted backgrounds
--muted-foreground--ds-text-mutedMuted text
--accent--ds-color-primary-50Accent highlights
--accent-foreground--ds-color-primary-900Text on accent
--destructive--ds-color-error-500Destructive actions
--border--ds-border-defaultDefault borders
--input--ds-border-defaultInput borders
--ring--ds-color-primary-500Focus rings
--radius0pxBorder radius (base-lyra: sharp)

cn() upgrade

The existing cn() utility is upgraded to use clsx + tailwind-merge for robust class name handling:

// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

tailwind-merge (^3.5.0) intelligently resolves Tailwind class conflicts (e.g., p-4 vs p-2), ensuring that component consumers can override styles without unexpected cascading behavior.

class-variance-authority

class-variance-authority (CVA, ^0.7.1) provides type-safe variant definitions for components:

import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        primary: "bg-primary text-primary-foreground hover:bg-primary/90",
        secondary: "bg-secondary text-secondary-foreground border border-border hover:bg-accent",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        danger: "bg-destructive text-white hover:bg-destructive/90",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
);

CVA ensures that variant combinations are type-checked at compile time and that invalid combinations produce TypeScript errors.

Migration path

The migration from the current 4 CSS Modules components to shadcn/ui + Base UI follows this path:

  1. New components: Added via shadcn CLI, customized, and exposed through Module Federation.
  2. Existing components rebuilt: Button, Modal, and Typography are rebuilt using shadcn/ui equivalents with CVA + Base UI + Tailwind utility classes. The CSS Module files (.module.css) are removed.
  3. ThemeProvider remains custom: The ThemeProvider is not a shadcn component. It remains custom but is updated to also set shadcn CSS variables alongside the existing --ds-* tokens.

Adding components workflow

# 1. Add a shadcn component
npx shadcn@latest add dialog

# 2. Component is placed in src/components/ui/dialog.tsx
# 3. Customize: adjust tokens, variants, brand styles

# 4. Expose via Module Federation in rsbuild.config.ts
#    "./Dialog": "./src/components/ui/dialog.tsx"

# 5. Add Storybook stories
#    src/components/ui/dialog.stories.tsx

# 6. Export types from the npm package entry point

Consequences

Positive

  • 60+ accessible components available immediately, covering dialogs, dropdowns, tooltips, tabs, command palettes, popovers, sheets, and more.
  • Owned source code: Components live in src/components/ui/, fully modifiable without waiting for upstream releases.
  • Base UI actively maintained: Dedicated team at MUI, regular releases, clear roadmap, React 19 support.
  • Tailwind alignment: Full Tailwind CSS v4 integration with OKLCH colors and CSS-first configuration.
  • React 19 compatibility: Base UI is built for React 19, no compatibility shims needed.
  • RTL support: Base UI primitives support right-to-left layouts out of the box.
  • 5 visual styles: Teams can evaluate alternative styles (default, new-york, miami, base-lyra, base-pico) for different product contexts.
  • Type-safe variants: CVA provides compile-time checking of component variant combinations.

Negative

  • Base UI ecosystem less mature than Radix: Fewer community resources, third-party tutorials, and codemods compared to the established Radix ecosystem. Tooling is still growing.
  • render prop instead of asChild: Base UI uses a render prop for component composition rather than Radix's asChild pattern. Developers familiar with Radix will need to learn the new pattern.
  • Manual Rsbuild setup: The shadcn CLI does not auto-detect Rsbuild as a framework, requiring manual components.json configuration.
  • New dependencies: Six new runtime/dev dependencies (@base-ui/react, class-variance-authority, clsx, tailwind-merge, lucide-react, tw-animate-css).
  • Token mapping effort: The shadcn CSS variable namespace must be mapped to the existing --ds-* token system. This is a one-time setup cost.
  • Known issue: The "use client" directive on the Button component can cause issues in certain build configurations (shadcn/ui #9428). Workaround: ensure the Rsbuild configuration does not strip client directives.

New Dependencies

PackageVersionPurpose
@base-ui/react^1.2.0Accessible headless primitives
class-variance-authority^0.7.1Type-safe component variants
clsx^2.1.1Conditional class names
tailwind-merge^3.5.0Tailwind class conflict resolution
lucide-react^0.576.0Icon library
tw-animate-css^1.4.0Tailwind v4 animation utilities

Alternatives Considered

shadcn/ui + Radix UI

The original default for shadcn/ui. Radix provides excellent accessibility primitives and has been the standard for headless component libraries. However, with the project in de facto wind-down -- one remaining sporadic maintainer, 774+ open issues, no roadmap, and the co-creator's public departure -- building the platform's long-term component foundation on Radix carries unacceptable maintenance risk.

Radix UI primitives alone (without shadcn)

Using Radix primitives directly and building all styling from scratch. This approach requires significantly more effort to create styled, variant-rich components. It is also subject to the same Radix maintenance concerns described above, without the benefit of shadcn/ui's pre-built component implementations and CLI tooling.

Material UI (MUI)

MUI's component library is comprehensive and well-maintained. However, its CSS-in-JS approach (Emotion) conflicts with the platform's Tailwind CSS strategy. MUI components are installed as npm dependencies, not owned source code, limiting customization. The runtime style injection also adds overhead that Tailwind's utility-first approach avoids.

Chakra UI

Chakra UI provides an accessible component library with a good developer experience. However, it uses Panda CSS for styling, which conflicts with the platform's Tailwind CSS investment. Chakra's runtime style props add overhead compared to Tailwind's compile-time utility classes.

Headless UI (Tailwind Labs)

Headless UI is built by the Tailwind Labs team and integrates naturally with Tailwind CSS. However, it provides only ~10 components compared to shadcn/ui's 35+. It lacks pre-styled implementations, requiring the team to build all visual variants from scratch.

Build from scratch

Continuing to build components from scratch at the current pace (4 components to date). This approach is unsustainable: the accessibility expertise required for each component is expensive, the development time is prohibitive, and the platform teams need 60+ components to be productive.