造 SaaS 产品,你可能花 60% 的前端时间在 UI 细节上:按钮状态、表单验证、Modal 层级、表格排序、Toast 动画……如果每个都从零写,你的 MVP 永远出不来。
但选错组件库的代价同样高昂:定制受限、包体积爆炸、升级断裂、设计感同质化。这篇帮你理清 什么时候用什么,以及 怎么组合。
原子层:无样式、纯行为的底层原语(Radix UI、Headless UI、Ark UI)。处理键盘导航、ARIA、焦点管理,但不提供任何视觉样式。
组件层:在原子层上叠加设计系统,提供可用的 UI 组件(shadcn/ui、Ant Design、MUI)。
模板块:完整页面布局和业务模板(Dashboard 模板、SaaS Starter Kit),通常由社区或商业产品提供。
严格来说 Tailwind 不是组件库,但它是 2024-2026 React 生态几乎所有现代组件库的底层样式引擎。不理解 Tailwind,就无法理解 shadcn/ui 等新范式。
原子化 CSS 框架。不提供组件,只提供 utility class:flex、px-4、text-sm、rounded-lg。你通过组合这些 class 来构建 UI,而非写自定义 CSS。
Tailwind v4 用 Rust 引擎 (Oxide) 扫描模板,只生成用到的 CSS。生产包通常 5-15KB gzip,远小于传统 CSS-in-JS。
间距、颜色、字号都由 tailwind.config 控制,天然保证设计一致性。不用 Tailwind 时,5 个开发者写 5 种 margin。
{/* Tailwind v4 — 无需配置文件,CSS-first 配置 */}
<div class="rounded-2xl border border-border bg-card p-6 shadow-sm">
<h3 class="text-lg font-semibold">Pro</h3>
<p class="mt-2 text-3xl font-bold">
$29<span class="text-sm font-normal text-muted">/月</span>
</p>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
<li>✓ 无限项目</li>
<li>✓ 高级分析</li>
<li>✓ 优先支持</li>
</ul>
<button class="mt-6 w-full rounded-lg bg-primary px-4 py-2.5
text-sm font-medium text-primary-foreground
hover:bg-primary/90 transition-colors">
开始使用
</button>
</div>
tailwind.config.js,所有配置写在 CSS 里 @theme { --color-primary: #6c8aff; }content: [...] 配置@apply 内嵌套@config 指令支持旧配置文件迁移Radix UI 是 React 生态最成熟的 Headless 组件原语库。它提供纯行为、零样式的组件原语——处理所有可访问性、键盘交互、焦点管理的脏活累活,样式完全交给你。
一组无样式的 React 组件原语:Dialog、Dropdown Menu、Popover、Toast、Tabs、Accordion、Tooltip 等。每个组件只负责 WAI-ARIA 规范的行为实现。
| 能力 | Radix 做了什么 | 为什么重要 |
|---|---|---|
| 焦点管理 | Modal 打开时焦点 trap,关闭后恢复 | 手动实现至少 50 行代码且容易出错 |
| 键盘导航 | 方向键选择、Enter 确认、Esc 关闭 | WAI-ARIA 规范有上百条交互规则 |
| ARIA 属性 | 自动添加 role、aria-label、aria-expanded | 可访问性审计必过项 |
| 滚动锁定 | Modal 打开时禁止背景滚动 | iOS Safari 滚动穿透是经典 bug |
| Portal 渲染 | 弹出层渲染到 body 避免 overflow 裁剪 | z-index 层级问题的根因之一 |
import * as Dialog from '@radix-ui/react-dialog'
function ConfirmDialog({ open, onOpenChange, onConfirm }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed top-1/2 left-1/2
-translate-x-1/2 -translate-y-1/2
bg-white rounded-lg p-6 shadow-xl">
<Dialog.Title className="text-lg font-semibold">
确认删除?
</Dialog.Title>
<Dialog.Description className="mt-2 text-gray-500">
此操作不可撤销,数据将永久删除。
</Dialog.Description>
<div className="mt-4 flex justify-end gap-2">
<Dialog.Close className="px-4 py-2 rounded
border hover:bg-gray-50">取消</Dialog.Close>
<button onClick={onConfirm}
className="px-4 py-2 rounded bg-red-500
text-white hover:bg-red-600">
删除
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
shadcn/ui 重新定义了组件库的分发方式:不是 npm 包,是源码。你用 CLI 把组件代码直接复制到项目里,然后随意修改。它的流行代表了 React 生态对「可控性」的极致追求。
基于 Radix UI + Tailwind CSS 的组件集合。每个组件都是你项目源码的一部分——可以改样式、改逻辑、改任何东西。没有 node_modules 依赖,没有版本锁定。
以 Dialog 组件为例:
// npx shadcn@latest add dialog 后,你的项目里多了一个文件:
// components/ui/dialog.tsx
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@/lib/utils" // class 合并工具
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in
data-[state=closed]:animate-out", // ← 你可以直接改这个
className
)}
{...props}
/>
))
// ... 完全可编辑的源码
源码在你手里。改样式改逻辑改任何东西,不需要 fork、patch-package、或者等上游合并 PR。
只安装你用到的组件。不用的组件不存在于你的 node_modules。Button 组件不依赖 Table。
通过 CSS 变量切换主题:--primary、--background、--accent。暗色/亮色模式零成本。
每个组件都是 Radix Primitive + Tailwind 的组合。你可以拆开重组,构建自己的变体。
/* globals.css — shadcn/ui 的主题核心 */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--accent: 210 40% 96.1%;
--muted: 210 40% 96.1%;
--destructive: 0 84.2% 60.2%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--accent: 217.2 32.6% 17.5%;
--destructive: 0 62.8% 30.6%;
}
}
/* 一行切换品牌色 — 所有组件自动生效 */
:root {
--primary: 250 89% 65%; /* 从蓝色改为紫色 */
}
| 组件 | 用途 | 替代手动实现 |
|---|---|---|
| DataTable | 数据表格(排序/筛选/分页) | TanStack Table + shadcn 样式 |
| Form | 表单验证 + 错误提示 | React Hook Form + Zod + shadcn |
| Command | 搜索面板 / 命令面板 (⌘K) | cmdk 库 + shadcn 样式 |
| Sidebar | 侧边栏导航 | shadcn sidebar 组件 |
| Chart | 数据可视化 | Recharts + shadcn 主题 |
| Calendar | 日期选择 | react-day-picker + shadcn |
npx shadcn@latest diff 检查npx shadcn@latest init 初始化,然后只 add 你需要的组件。不要一次装全部。用 shadcn@latest diff 定期检查更新。自定义 CSS 变量来避免「shadcn 脸」。
蚂蚁集团出品,国内企业级 SaaS 最广泛使用的组件库。开箱即用、功能全面、中文生态完善。如果你做 B 端管理后台,Ant Design 是默认选项。
一套完整的企业级设计体系和 React 组件库。包含 60+ 组件、设计规范、设计资源(Figma/Sketch)、Pro 模板。从表单到表格到图表,几乎覆盖所有 B 端场景。
ProTable/ProForm/ProLayout 直接提供完整业务组件。管理后台 MVP 速度最快。
文档中文、社区中文、设计规范适配中文排版。国内招人最易上手。
import { ProTable } from '@ant-design/pro-components'
function UserManagement() {
return (
<ProTable
columns={[
{ title: '用户名', dataIndex: 'username' },
{ title: '邮箱', dataIndex: 'email', copyable: true },
{ title: '角色', dataIndex: 'role',
valueEnum: {
admin: { text: '管理员', status: 'Success' },
user: { text: '普通用户', status: 'Default' },
}
},
{ title: '注册时间', dataIndex: 'createdAt',
valueType: 'dateTime',
sorter: true,
},
{ title: '操作', valueType: 'option',
render: (_, record) => [
<a key="edit">编辑</a>,
<a key="delete">删除</a>,
]
},
]}
request={async (params) => {
const res = await fetch('/api/users', { params })
return { data: res.data, total: res.total, success: true }
}}
rowKey="id"
search={{ labelWidth: 'auto' }}
pagination={{ pageSize: 20 }}
toolBarRender={() => [
<Button key="add" type="primary">新增用户</Button>,
]}
/>
)
}
ConfigProvider,细粒度到组件级别@ant-design/compatible 帮助 v4 → v5 迁移StyleProviderStyleProvider layer={true} + extractStyle 配合。如果你用 Next.js App Router,务必测试 SSR 场景。
Google Material Design 的 React 实现。国际市场占有率最高的 React 组件库。如果你的 SaaS 面向全球用户,MUI 是最安全的选择。
Material Design 3 规范的 React 实现。包含 50+ 组件、主题系统、布局组件、数据展示组件。MUI Core 开源,MUI X (Pro/Premium) 提供高级数据组件。
import { DataGrid, GridColDef } from '@mui/x-data-grid'
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: '名称', width: 200, editable: true },
{ field: 'status', headerName: '状态', width: 120,
type: 'singleSelect',
valueOptions: ['active', 'inactive', 'pending'],
},
{ field: 'revenue', headerName: '收入', width: 130,
type: 'number',
valueFormatter: (v) => `$${v.toLocaleString()}`,
},
]
function RevenueTable() {
const { data, isLoading } = useRevenueData()
return (
<DataGrid
rows={data}
columns={columns}
pageSizeOptions={[10, 25, 50]}
checkboxSelection
disableRowSelectionOnClick
loading={isLoading}
sx={{ border: 'none' }}
/>
)
}
Tailwind CSS 团队出品的 Headless 组件库。比 Radix 轻量,组件更少但更精致。如果你用 Tailwind 但觉得 Radix 太重,这是替代。
import { Dialog, Transition } from '@headlessui/react'
function MyDialog({ isOpen, onClose }) {
return (
<Transition appear show={isOpen}>
<Dialog onClose={onClose} className="relative z-50">
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center">
<Dialog.Panel className="w-full max-w-md
rounded-2xl bg-white p-6">
<Dialog.Title>确认操作</Dialog.Title>
<Dialog.Description>确定要继续吗?</Dialog.Description>
</Dialog.Panel>
</div>
</Dialog>
</Transition>
)
}
跨框架 Headless 库(React/Vue/Solid)。比 Radix 支持更多框架。如果你用 Solid 或 Vue,Ark UI 是 Headless 的最佳选择。
开发者体验优先的组件库。API 设计优雅,但运行时 CSS-in-JS 限制了性能上限。v3 已迁移到 Tailwind + CSS 变量方案,值得关注。
Tailwind CSS 插件形式的组件库。不提供 React 组件,只提供 CSS class:btn、card、modal。适合多框架项目或非 React 项目。
<!-- daisyUI — 框架无关 -->
<button class="btn btn-primary">Click me</button>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">标题</h2>
<p>内容</p>
</div>
</div>
视觉设计突出的 React 组件库。动画和默认样式比 shadcn/ui 更丰富,但定制自由度低。适合注重外观的 C 端产品。
数据可视化组件。SaaS Dashboard 几乎必用。shadcn/ui 的 Chart 组件基于 Recharts。
| 维度 | shadcn/ui | Ant Design | MUI | Chakra UI | Headless UI |
|---|---|---|---|---|---|
| 分发方式 | 源码复制 | npm 包 | npm 包 | npm 包 | npm 包 |
| 样式方案 | Tailwind + CSS Var | CSS-in-JS (v5) | CSS-in-JS (emotion) | CSS-in-JS → Tailwind (v3) | Tailwind |
| 可定制性 | 极高 | 中等 | 中等 | 较高 | 极高 |
| 包体积 | 极小 | 大 | 大 | 中等 | 小 |
| 组件数量 | 40+ | 60+ | 50+ | 50+ | 12 |
| SSR 兼容 | 完美 | 需配置 | 需配置 | 需配置 | 完美 |
| TypeScript | 原生 | 原生 | 原生 | 原生 | 原生 |
| 高级组件 | 需组合 | ProComponents | MUI X (付费) | 无 | 无 |
| 设计语言 | 极简 / 无 | Ant Design | Material | Chakra | 无 |
| 升级难度 | 手动 diff | 破坏性变更 | 破坏性变更 | 中等 | 低 |
| 适合场景 | 产品级 SaaS | 企业后台 | 国际化产品 | 快速原型 | 自定义设计 |
你在做什么类型的产品?
│
├─ B 端管理后台 / 企业工具
│ ├─ 团队在中国?
│ │ └─ ✅ Ant Design + ProComponents(最快出活)
│ └─ 团队国际化?
│ └─ ✅ MUI + MUI X(全球生态最稳)
│
├─ C 端产品 / 创业公司 SaaS
│ ├─ 需要独特设计感?
│ │ └─ ✅ shadcn/ui + 自定义 CSS 变量(完全可控)
│ └─ 快速 MVP,设计不重要?
│ └─ ✅ shadcn/ui 默认主题 + Tailwind
│
├─ 自己造设计系统
│ ├─ React?
│ │ └─ ✅ Radix UI + Tailwind(行为 + 样式分离)
│ └─ Vue / Solid?
│ └─ ✅ Ark UI + Tailwind(跨框架 Headless)
│
└─ 非 React 项目
├─ Vue?
│ ├─ ✅ Naive UI(Vue 3 原生,国人开发)
│ └─ ✅ Element Plus(Vue 2/3,企业级)
├─ Svelte?
│ └─ ✅ shadcn-svelte + Bits UI
└─ 框架无关?
└─ ✅ daisyUI(Tailwind 插件形式)
# 初始化
npx create-next-app@latest my-saas --typescript --tailwind --app
cd my-saas
npx shadcn@latest init
# 按需添加组件
npx shadcn@latest add button input card dialog table form
npx shadcn@latest add sidebar command calendar chart
# 数据表格 — TanStack Table + shadcn
npm install @tanstack/react-table
# 表单 — React Hook Form + Zod
npm install react-hook-form @hookform/resolvers zod
适用:90% 的 SaaS 场景。包体积小、定制自由、SSR 完美、社区活跃。
# Ant Design Pro 脚手架
npx create-umi@latest
# 选择 Ant Design Pro 模板
# 或手动搭建
npm install antd @ant-design/pro-components @ant-design/icons
npm install @ant-design/pro-layout @ant-design/pro-table @ant-design/pro-form
# 状态管理
npm install @umijs/max # 内置 model、request、access
适用:国内企业后台、管理面板。出活最快,但长期定制成本高。
# 只安装需要的 Radix 原语
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
npm install @radix-ui/react-popover @radix-ui/react-toast
# 自己写样式组件 — 100% 控制
# utils/cn.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
# 自己封装 Button
const buttonVariants = {
default: "bg-primary text-white hover:bg-primary/90",
outline: "border border-input hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive: "bg-destructive text-white hover:bg-destructive/90",
}
function Button({ variant = "default", className, ...props }) {
return (
<button
className={cn(
"inline-flex items-center justify-center rounded-md
px-4 py-2 text-sm font-medium transition-colors",
buttonVariants[variant],
className
)}
{...props}
/>
)
}
适用:打造自有设计系统、需要极致品牌差异化。
表单是 SaaS 产品的核心交互——注册、登录、设置、搜索、CRUD。选错表单方案,你的开发体验和用户体验都会很痛苦。
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem,
FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
// Zod schema — 类型安全的验证规则
const signupSchema = z.object({
email: z.string().email('请输入有效邮箱'),
password: z.string()
.min(8, '密码至少 8 位')
.regex(/[A-Z]/, '需要至少一个大写字母')
.regex(/[0-9]/, '需要至少一个数字'),
confirmPassword: z.string(),
}).refine(d => d.password === d.confirmPassword, {
message: '两次密码不一致',
path: ['confirmPassword'],
})
type SignupValues = z.infer<typeof signupSchema>
function SignupForm() {
const form = useForm<SignupValues>({
resolver: zodResolver(signupSchema),
defaultValues: { email: '', password: '', confirmPassword: '' },
})
async function onSubmit(values: SignupValues) {
// values 类型完全安全,不需要 any
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(values),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4">
<FormField control={form.control} name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* password, confirmPassword 同理 */}
<Button type="submit" className="w-full">
注册
</Button>
</form>
</Form>
)
}
几乎所有 SaaS 都有列表页——用户列表、订单列表、日志列表。数据表格的选型直接影响用户体验和开发效率。
import {
useReactTable, getCoreRowModel, getSortedRowModel,
getFilteredRowModel, getPaginationRowModel,
flexRender,
} from '@tanstack/react-table'
// 列定义 — 类型安全
const columns = [
columnHelper.accessor('name', {
header: '名称',
cell: info => info.getValue(),
}),
columnHelper.accessor('status', {
header: '状态',
cell: info => <Badge variant={info.getValue() === 'active'
? 'default' : 'secondary'}>{info.getValue()}</Badge>,
filterFn: 'equals',
}),
columnHelper.accessor('createdAt', {
header: '创建时间',
cell: info => formatDate(info.getValue()),
sortingFn: 'datetime',
}),
]
function DataTable({ data }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map(hg => (
<TableRow key={hg.id}>
{hg.headers.map(header => (
<TableHead key={header.id}
onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header,
header.getContext())}
{{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string]}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map(row => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell,
cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{/* 分页控制 — shadcn Pagination */}
<div className="flex items-center justify-end p-4">
<Button variant="outline" size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}>上一页</Button>
<span className="text-sm text-muted-foreground mx-2">
第 {table.getState().pagination.pageIndex + 1} 页
</span>
<Button variant="outline" size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}>下一页</Button>
</div>
</div>
)
}
| 方案 | 灵活度 | 开箱功能 | 适合场景 |
|---|---|---|---|
| TanStack Table + shadcn | 极高 | 基础排序/筛选 | 产品级 SaaS,需定制 |
| Ant Design ProTable | 中等 | 全部内置 | 企业后台,快速出活 |
| MUI DataGrid (Pro) | 较高 | 全部内置 + Excel 导出 | 国际化企业 SaaS |
| AG Grid | 极高 | 最全 (百万行) | 金融/数据密集型 |
多数 SaaS 产品的 UI 库选择决定了产品的视觉基调。主题系统决定了你能走多远。
/* globals.css — 品牌定制 */
@layer base {
:root {
/* 品牌: OpenClaw 紫 */
--primary: 263 70% 65%;
--primary-foreground: 0 0% 100%;
/* 背景: 深蓝灰 */
--background: 222 47% 5%;
--foreground: 210 40% 98%;
/* 强调: 亮青 */
--accent: 187 92% 69%;
--accent-foreground: 222 47% 5%;
/* 圆角: 稍大,更柔和 */
--radius: 0.75rem;
}
}
/* 为 SaaS 仪表盘定制卡片风格 */
.card-dashboard {
@apply rounded-xl border bg-card/50 backdrop-blur
shadow-lg shadow-black/5 transition-all;
}
.card-dashboard:hover {
@apply shadow-xl shadow-primary/5 border-primary/20;
}
.dark class 改变 :root 变量值,组件无需感知模式#000000 太刺眼。用 hsl(222, 47%, 5%) 这种带色调的深色hsl(210, 40%, 90%) 而非纯白 #FFFFFFSaaS 的 UX 竞争在微交互层面。流畅的过渡让产品感觉「活」的,而非「生硬的」。
// shadcn/ui 的 Dialog 已经内置动画
// data-[state=open]:animate-in
// data-[state=closed]:animate-out
// 自定义动画
const variants = {
hidden: { opacity: 0, y: 10 },
enter: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 },
}
// Framer Motion — 更复杂的动画
import { motion, AnimatePresence } from 'framer-motion'
function PageTransition({ children }) {
return (
<AnimatePresence mode="wait">
<motion.div
initial="hidden"
animate="enter"
exit="exit"
variants={variants}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
transform 和 opacity 做动画。这两个属性有 GPU 加速。避免动画 width、height、top、left——它们触发布局重排,卡顿的根源。
如果你的 SaaS 后端是 Python/Go 且前端用 Vue,这里有对应选项。
国人 07akioni 开发,Vue 3 Composition API 原生编写。Tree-shaking 友好,TypeScript 原生支持,主题系统基于 CSS 变量。设计感比 Element Plus 现代。
import { NDataTable, NButton, NSpace } from 'naive-ui'
const columns = [
{ title: '名称', key: 'name' },
{ title: '状态', key: 'status' },
{ title: '操作', key: 'actions',
render(row) {
return h(NSpace, null, () => [
h(NButton, { size: 'small' }, () => '编辑'),
h(NButton, { size: 'small', type: 'error' }, () => '删除'),
])
}
},
]
Element UI 的 Vue 3 版本。国内企业 Vue 项目的事实标准。组件数量多,文档中文,社区成熟。但设计语言偏旧(Element 2.0 风格),定制体验不如 Naive UI。
社区驱动的 Vue 3 移植版,基于 Radix Vue(Radix UI 的 Vue 版本)+ Tailwind CSS。API 设计和理念与 React 版一致。
如果你的 SaaS 面向欧美市场,WCAG 2.1 AA 合规是法律要求,不是锦上添花。2025 年欧洲无障碍法案 (EAA) 生效,不合规的产品可能被禁止销售。
| 需求 | 自己实现 | Radix / Headless UI |
|---|---|---|
| 键盘导航 | 手写 keydown 处理 | 内置方向键/Tab/Esc |
| ARIA 角色 | 手动添加 20+ 属性 | 自动生成 |
| 焦点管理 | 手写 focus-trap | 内置 Modal 焦点陷阱 |
| 屏幕阅读器 | 需逐个测试 | 预验证通过 |
| 高对比度 | 需手动调整 | Tailwind 配色系统 |
// 一个可访问的 Tabs 组件 — Radix 自动处理所有 a11y
import * as Tabs from '@radix-ui/react-tabs'
function SettingsTabs() {
return (
<Tabs.Root defaultValue="account">
<Tabs.List className="flex border-b">
<Tabs.Trigger value="account"
className="px-4 py-2 data-[state=active]:border-b-2
data-[state=active]:border-primary">
账户
</Tabs.Trigger>
<Tabs.Trigger value="security"
className="px-4 py-2 data-[state=active]:border-b-2
data-[state=active]:border-primary">
安全
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account" className="p-4">
账户设置
</Tabs.Content>
<Tabs.Content value="security" className="p-4">
安全设置
</Tabs.Content>
</Tabs.Root>
)
}
| 方案 | 基础大小 (gzip) | 全量大小 | Tree-shaking |
|---|---|---|---|
| shadcn/ui + Tailwind | 5-15KB (CSS) | 按需 | ✅ 天然按需 |
| Ant Design | ~80KB (core) | ~300KB+ | ✅ v5 支持 |
| MUI | ~80KB (core) | ~350KB+ | ✅ 支持 |
| Chakra UI | ~50KB | ~200KB | ✅ 支持 |
| Headless UI | ~15KB | ~15KB | ✅ 支持 |
Ant Design v5 和 MUI 使用 runtime CSS-in-JS (emotion/styled)。每次组件渲染,样式在运行时计算并注入。这意味着:
shadcn/ui + Tailwind 的优势:样式在构建时生成,零运行时开销。SSR 开箱即用。
UI 组件库看似与 Agent 无关,但在 AI-native SaaS 中,二者深度交叉:
ScrollArea + 自定义 MessageBubble 是主流方案animate-pulse 做 loading 指示| 组件 | 用途 | 推荐实现 |
|---|---|---|
| StreamingText | 逐字显示 LLM 输出 | 自定义 + react-markdown |
| CodeBlock | 语法高亮代码展示 | Shiki / Prism + shadcn 样式 |
| ToolCallCard | 展示 Agent 工具调用 | shadcn Card + Collapsible |
| ApprovalDialog | 人工审批 Agent 操作 | shadcn AlertDialog |
| TokenCounter | 显示 token 用量 | shadcn Badge + 自定义 |
| ThinkingIndicator | 显示 Agent 思考中 | tailwindcss-animate 脉冲 |
症状:Ant Design 的 Modal + shadcn 的 Button + MUI 的 Table。看起来每个都是「最好的」,但 CSS 冲突、样式不一致、包体积爆炸。
解法:选一个组件库做主力,其余用 Headless 原语补充。例如:shadcn/ui 主力 + Radix 补充特殊组件。
症状:SSR 样式闪烁 (FOUC) + hydration mismatch。
解法:在 layout.tsx 中用 <StyleProvider layer> + extractStyle。或干脆别在 Next.js App Router 里用 Ant Design——shadcn/ui 才是原生选择。
症状:npx shadcn@latest add dialog 覆盖了你的自定义修改。
解法:用 npx shadcn@latest diff dialog 检查变更,手动合并。或将自定义放在 components/ui/ 之外的 wrapper 层。
症状:Server Component 里使用 styled-components / emotion 报错。
解法:RSC 不支持 CSS-in-JS。在 RSC 中只能用 Tailwind / CSS Modules / CSS 变量。CSS-in-JS 限定在 Client Component 内。
症状:6 个月后产品里 5 种灰色、3 种圆角、7 种字号。
解法:Day 1 就定义 tailwind.config 或 CSS 变量。颜色、间距、圆角、字号统一管理。宁可多花 1 天定义设计约束,省 100 天修复不一致。