ADR 011: shadcn/ui as Component Foundation
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/reactreached 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.
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
| Factor | Radix UI | Base UI |
|---|---|---|
| Active maintainers | 1 (sporadic) | Dedicated team at MUI |
| Corporate backing | None (Modulz dissolved) | MUI (profitable, established) |
| Last release | November 2025 | Active monthly releases |
| Open issues | 774+ | Actively triaged |
| React 19 support | Partial | Full |
| Creator recommendation | Co-creator moved to Base UI | Created by original Radix authors |
| Composition pattern | asChild prop | render prop |
| shadcn/ui support | Yes (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
@themedirective replaces the oldtailwind.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 variable | Maps to | Purpose |
|---|---|---|
--background | --ds-bg-primary | Page background |
--foreground | --ds-text-primary | Default text color |
--primary | --ds-color-primary-600 | Primary brand color |
--primary-foreground | #FFFFFF | Text on primary |
--secondary | --ds-bg-secondary | Secondary backgrounds |
--secondary-foreground | --ds-text-primary | Text on secondary |
--muted | --ds-color-neutral-100 | Muted backgrounds |
--muted-foreground | --ds-text-muted | Muted text |
--accent | --ds-color-primary-50 | Accent highlights |
--accent-foreground | --ds-color-primary-900 | Text on accent |
--destructive | --ds-color-error-500 | Destructive actions |
--border | --ds-border-default | Default borders |
--input | --ds-border-default | Input borders |
--ring | --ds-color-primary-500 | Focus rings |
--radius | 0px | Border 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:
- New components: Added via shadcn CLI, customized, and exposed through Module Federation.
- 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. - 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.
renderprop instead ofasChild: Base UI uses arenderprop for component composition rather than Radix'sasChildpattern. 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.jsonconfiguration. - 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
| Package | Version | Purpose |
|---|---|---|
@base-ui/react | ^1.2.0 | Accessible headless primitives |
class-variance-authority | ^0.7.1 | Type-safe component variants |
clsx | ^2.1.1 | Conditional class names |
tailwind-merge | ^3.5.0 | Tailwind class conflict resolution |
lucide-react | ^0.576.0 | Icon library |
tw-animate-css | ^1.4.0 | Tailwind 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.
Related Documentation
- Architecture Overview -- Technology stack and system context
- Design System -- Dual distribution, theming, component integration details
- Module Federation -- MF remote distribution of components
- ADR-008: Rsbuild Bundler -- Build tool for MF output
- Local Development -- Local design system development workflow