UI libraries accelerate delivery—if you pick the right level of abstraction and treat them like infrastructure. This guide helps you evaluate, customize, and migrate with minimal disruption.
Also read: Web Design Best Practices · Web Application Design · UI Animation
Headless vs. Styled vs. Hybrids
| Type | Examples | Pros | Cons |
|---|---|---|---|
| Headless | Radix, Headless UI | Max control, a11y guarantees | You own all styling |
| Styled | MUI, Chakra, AntD, Mantine | Faster to ship, theme systems | Visual drift if themeing is shallow |
| Hybrid / Code-gen | shadcn/ui | Own the source, Radix under the hood | You maintain the copies |
Pick based on brand control needs, team skill, and time-to-market.
Evaluation Criteria (deep cut)
- API ergonomics: props that map to intention, not paint.
- A11y: keyboard order, focus traps, screen reader naming.
- Theming: tokens for color/space/radius; dark mode support.
- Motion: sensible transitions; easy to disable for reduced motion.
- Docs & community: recency, examples, maintenance.
- Bundle impact: ESM, treeshaking, partial imports.
- Interop: Tailwind, CSS-in-JS, or vanilla CSS.
Next.js + pnpm Integration
Install
pnpm add @radix-ui/react-dialog @radix-ui/react-popover @radix-ui/react-tooltip
# or
pnpm add @mui/material @emotion/react @emotion/styled
# or
pnpm dlx shadcn@latest init && pnpm dlx shadcn add button card dialogOptimize imports
// next.config.mjs
export default { experimental: { optimizePackageImports: ["lucide-react"] } };MUI theme override
// app/theme.tsx
"use client";
import { ThemeProvider, createTheme } from "@mui/material/styles";
const theme = createTheme({
palette: { mode: "light", primary: { main: "#111827" } },
shape: { borderRadius: 8 },
typography: { button: { textTransform: "none" } },
});
export function MuiThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}Radix + Tailwind (Dialog)
import * as Dialog from "@radix-ui/react-dialog";
export function Confirm({ open, onOpenChange, onConfirm }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Trigger className="btn">Delete</Dialog.Trigger>
<Dialog.Content className="rounded-xl bg-white p-6 shadow-xl">
<Dialog.Title>Confirm deletion</Dialog.Title>
<div className="mt-4 flex gap-3">
<button className="btn-primary" onClick={onConfirm}>
Yes
</button>
<Dialog.Close className="btn">Cancel</Dialog.Close>
</div>
</Dialog.Content>
</Dialog.Root>
);
}Customization Strategy
- Define tokens first (color, space, radius, shadows, motion).
- Theming pass (global), then component-level overrides.
- Create a “playground” route to audition states (hover, focus, error).
- Build compound components for repeated patterns (FilterBar, Toolbar).
Migration Without a Rewrite
- Strangle pattern: wrap old components with adapters; swap incrementally.
- Route-by-route replacement; keep styles side-by-side temporarily.
- Snapshot test visual regressions; lock down critical flows with e2e.
- Document mapping: Old → New components; agree on “good enough.”
Pitfalls to Avoid
- Mixing multiple heavy kits on one surface.
- Overriding styles via specificity wars; prefer tokens/vars.
- Loading entire icon packs; import per icon.
- Ignoring
prefers-reduced-motionand keyboard paths.
Checklist
- Tokens set and documented
- A11y verified for key components
- Imports optimized (tree-shake friendly)
- Theming unified across routes
- Migration plan with milestones
Continue: Web Design Best Practices · Web Application Design · UI Animation

