Switching the UI library
Only change files inside apps/foundation/src/ui/. Consumer code keeps importing from @foundation/ui. This guide demonstrates the swap using three popular libraries (MUI, Chakra, React Aria), with example contract→library mapping and step-by-step implementation per tab.
How to switch
- Leave unchanged: all
types.ts,tokens.ts,index.ts, and consumer code. - Update
mapping.tswith the new library's size/variant/color scale. - Replace each component's
index.tsx— import from the new library, pass mapped props from../mapping, and drop or adjust props the library doesn't support.
Per-library: contract + implementation
Pick a library tab. Each tab shows the contract mapping (what the library expects) and implementation steps for that library.
- Radix Themes
- MUI
- Chakra
- React Aria
Contract → Radix Themes expects
| Our contract | Radix Themes expects |
|---|---|
SemanticSize: xs, sm, md, lg | size: "1", "2", "3", "4" |
SemanticSizeCompact: sm, md, lg | size: "1", "2", "3" |
ButtonVariant: solid, subtle, surface, outline, transparent | variant: subtle→soft, transparent→ghost; rest same |
IconButtonVariant: solid, subtle, outline, transparent | variant: subtle→soft, transparent→ghost; rest same |
AccentColor: gray, blue, red, orange, green | color: same names |
CardVariant: surface, classic, transparent | variant: surface, classic, ghost (transparent→ghost) |
Implementation
mapping.ts— Map our contract to Radix's scale and variant names. Add mappers for Card, IconButton, etc. per the contract table.
// Full example — same shape for other libs: map contract → library types (see their docs for exact types)
export function mapSemanticSize(size: SemanticSize): "1" | "2" | "3" | "4" {
const map: Record<SemanticSize, "1" | "2" | "3" | "4"> = { xs: "1", sm: "2", md: "3", lg: "4" };
return map[size];
}
export function mapButtonVariant(variant: ButtonVariant): "solid" | "soft" | "surface" | "outline" | "ghost" {
switch (variant) {
case "subtle": return "soft";
case "transparent": return "ghost";
default: return variant;
}
}
Button/index.tsx— Import from@radix-ui/themes, re-export types from./types, pass mapped size/variant/color, forward ref and spread rest.
import { forwardRef } from "react";
import { Button as RadixButton } from "@radix-ui/themes";
import type { ButtonProps } from "./types";
import { mapSemanticSize, mapButtonVariant } from "../mapping";
export type * from "./types";
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ size, variant, color, ...rest }, ref) => (
<RadixButton
ref={ref}
size={mapSemanticSize(size)}
variant={mapButtonVariant(variant)}
color={color}
{...rest}
/>
)
);
Other components (Card, IconButton, etc.): same pattern—add mappers in mapping.ts and wire them in that component's index.tsx.
Radix Themes — confirm prop names for your version.
Contract → MUI expects
| Our contract | MUI expects |
|---|---|
SemanticSize: xs, sm, md, lg | size: "small", "medium", "large" |
SemanticSizeCompact: sm, md, lg | same as above for compact components |
ButtonVariant: solid, subtle, surface, outline, transparent | variant: "contained", "outlined", "text"; map subtle/transparent to desired equivalents |
IconButtonVariant: solid, subtle, outline, transparent | same mapping idea as Button |
AccentColor: gray, blue, red, orange, green | color: "primary" (default), "secondary", "error", "success", "info", "warning", "inherit" |
CardVariant: surface, classic, transparent | variant: "elevation" | "outlined" only; Card has no size prop—accept in our contract but don't pass to MUI |
Implementation
mapping.ts— MapSemanticSizeto MUI Button'ssize:"small"|"medium"|"large". MapButtonVarianttovariant:"contained"|"outlined"|"text". MapAccentColortocolor:"primary"|"secondary"|"error"|"success"|"info"|"warning"|"inherit". Add mappers for other components per the contract table.
// Example: use MUI's actual types from their docs
export function mapSemanticSize(size: SemanticSize): "small" | "medium" | "large" {
const map: Record<SemanticSize, "small" | "medium" | "large"> = {
xs: "small", sm: "small", md: "medium", lg: "large",
};
return map[size];
}
export function mapButtonVariant(variant: ButtonVariant): "contained" | "outlined" | "text" {
const map: Record<ButtonVariant, "contained" | "outlined" | "text"> = {
solid: "contained",
outline: "outlined",
subtle: "text",
surface: "text",
transparent: "text",
};
return map[variant];
}
Button/index.tsx— Import from@mui/material, pass mapped props; omit contract props MUI doesn't support (e.g. asChild, highContrast).
import { forwardRef } from "react";
import MuiButton from "@mui/material/Button";
import type { ButtonProps } from "./types";
import { mapSemanticSize, mapButtonVariant, mapButtonColor } from "../mapping";
export type * from "./types";
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ size, variant, color, ...rest }, ref) => (
<MuiButton
ref={ref}
size={mapSemanticSize(size)}
variant={mapButtonVariant(variant)}
color={mapButtonColor(color)}
{...rest}
/>
)
);
Other components: same pattern; drop or adjust props the library doesn't support (e.g. if a component has no size, accept it in our props but don't pass it down).
MUI Button — confirm current API; other components follow the same idea.
This section follows Chakra v3 (Button uses colorPalette). If you use Chakra v2, use colorScheme instead and check their v2 docs for any other differences.
Contract → Chakra v3 expects
| Our contract | Chakra v3 expects |
|---|---|
SemanticSize: xs, sm, md, lg | size: "xs", "sm", "md", "lg" — same names; pass through or map if Chakra uses a different scale |
SemanticSizeCompact: sm, md, lg | size: "sm", "md", "lg" |
ButtonVariant: solid, subtle, surface, outline, transparent | variant: "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain"; map transparent→ghost. size: "xs" | "sm" | "md" | "lg" | "xl" |
IconButtonVariant: solid, subtle, outline, transparent | same as Button |
AccentColor: gray, blue, red, orange, green | colorPalette: same names (gray, blue, red, orange, green, etc.); pass through |
CardVariant: surface, classic, transparent | variant: "elevated" | "outline" | "subtle"; size: "sm" | "md" | "lg". Map e.g. surface→elevated, classic→outline, transparent→subtle |
Implementation
mapping.ts— Our size (xs/sm/md/lg) and color (gray, blue, red, orange, green) match Chakra v3—pass through or map. Map variant (transparent→ghost). Chakra v3 Button also hasxl; map our four sizes to a subset or extendSemanticSizeintokens.tsif you need xl. Add mappers for other components per the contract table.
// Example: Chakra v3 Button variant is "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain"
function mapButtonVariant(variant: ButtonVariant): "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain" {
// Only list overrides; same-named variants pass through via ?? variant
const map: Partial<Record<ButtonVariant, "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain">> = {
transparent: "ghost",
};
return map[variant] ?? variant;
}
Button/index.tsx— Import from@chakra-ui/react, pass size, variant, andcolorPalette(v3; usecolorSchemeif on v2).
import { forwardRef } from "react";
import { Button as ChakraButton } from "@chakra-ui/react";
import type { ButtonProps } from "./types";
import { mapButtonVariant } from "../mapping";
export type * from "./types";
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ size, variant, color, ...rest }, ref) => (
<ChakraButton
ref={ref}
size={size}
variant={mapButtonVariant(variant)}
colorPalette={color}
{...rest}
/>
)
);
Other components: same pattern. Card is compound (Card.Root, Card.Header, etc.); it has variant and size—map and pass through.
Chakra Button — confirm prop names; other components follow the same idea.
Contract → React Aria expects
| Our contract | React Aria expects |
|---|---|
SemanticSize: xs, sm, md, lg | className: e.g. btn-xs / btn-sm / btn-md / btn-lg (you define styles in CSS) |
SemanticSizeCompact: sm, md, lg | same pattern: map to class names (e.g. spinner-sm), style in CSS |
ButtonVariant: solid, subtle, surface, outline, transparent | data-variant: "primary" | "secondary" | "quiet" (solid→primary, transparent→quiet, rest→secondary); style via [data-variant="…"] in CSS |
IconButtonVariant: solid, subtle, outline, transparent | same as Button |
AccentColor: gray, blue, red, orange, green | className: e.g. btn-gray, btn-blue, … (you define styles in CSS) |
CardVariant: surface, classic, transparent | No Card component; map to className (e.g. card-surface, card-classic, card-transparent) and render a div |
Implementation — React Aria is unstyled; you supply styling via class names or data attributes.
mapping.ts— Map our contract to class names or data-attribute values (e.g. size→"btn-sm", variant→"primary"|"secondary"|"quiet", color→"btn-blue"). No library prop types; you choose the strings.
// Example: return class names or data values; you style them in CSS
function mapSemanticSize(size: SemanticSize): string {
const map: Record<SemanticSize, string> = { xs: "btn-xs", sm: "btn-sm", md: "btn-md", lg: "btn-lg" };
return map[size];
}
function mapButtonVariant(variant: ButtonVariant): "primary" | "secondary" | "quiet" {
switch (variant) {
case "solid": return "primary";
case "transparent": return "quiet";
default: return "secondary";
}
}
Button/index.tsx— Import fromreact-aria-components; passclassNameanddata-variantfrom mappers, style in CSS.
import { forwardRef } from "react";
import { Button as AriaButton } from "react-aria-components";
import type { ButtonProps } from "./types";
import { mapSemanticSize, mapButtonVariant, mapButtonColor } from "../mapping";
export type * from "./types";
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ size, variant, color, ...rest }, ref) => (
<AriaButton
ref={ref}
className={`${mapSemanticSize(size)} ${mapButtonColor(color)}`}
data-variant={mapButtonVariant(variant)}
{...rest}
/>
)
);
Other components: same pattern (className/data-* from mappers). For components React Aria doesn't provide, render a div with your class names; keep our contract so consumers don't change.
React Aria Components — see what components exist; you own the rest.