课程/2

2 课:shadcn/ui

组件库范式转移

shadcn/ui21st.devTweakCNmagic-ui2.5h

📋 本课概览

bash
┌─────────────────────────────────────────────────────────────────┐
│  🎯 核心观点:shadcn/ui 不是组件库,是代码分发平台               │
├─────────────────────────────────────────────────────────────────┤
│  📚 你将学到:                                                   │
│    • 理解 Open Code 哲学:为什么"开放代码"优于"安装依赖"       │
│    • 掌握 CLI 工具、Skills 和 MCP Server 的使用方法              │
│    • 深入 Registry 系统和新组件(Field、Item、Input Group 等)   │
│    • 了解 shadcn/ui 生态工具(magic-ui、TweakCN、v0.dev)        │
│    • 学会横向对比各类组件库,做出正确的技术选型                  │
│    • 实战:初始化项目、添加组件、用 AI 定制组件                  │
│    • 掌握创建自定义 Registry 和 AI 集成的方法                    │
└─────────────────────────────────────────────────────────────────┘

💡 2025 最新特性: Skills(AI 技能)、MCP Server(AI 工具协议)、Registry Index(注册表索引)、新组件(Field、Item、Button Group、Input Group、Spinner、Kbd、Empty)

课程结构导航


🎬 Opening:现场对比演示

场景设定

需求:把一个 Dialog 组件的关闭按钮从右上角移到左上角,同时加一个渐变背景的遮罩层

方案一:Ant Design(传统组件库)

jsx
import { Modal } from 'antd';

// AI 的尝试:通过 CSS 覆盖
<Modal open={isOpen} onCancel={handleClose} title="设置" className="custom-modal">
  <p>内容区域</p>
</Modal>
css
/* AI 不得不写一堆覆盖样式 */
.custom-modal .ant-modal-close {
  left: 12px;
  right: auto;
}
.custom-modal .ant-modal-mask {
  background: linear-gradient(135deg, rgba(0,0,0,0.6), rgba(0,0,0,0.3));
}

⚠️ 问题:AI 怎么知道 .ant-modal-close 这个类名?靠文档或训练数据记忆,版本升级后可能失效。

方案二:shadcn/ui(Copy-Paste 哲学)

jsx
// 直接修改 components/ui/dialog.tsx
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-gradient-to-br from-black/60 to-black/30",  // ✅ 直接改
      "data-[state=open]:animate-in data-[state=closed]:animate-out",
      className
    )}
    {...props}
  />
))

const DialogClose = React.forwardRef(({ className, ...props }, ref) => (
  <DialogPrimitive.Close
    ref={ref}
    className={cn(
      "absolute left-4 top-4 rounded-sm opacity-70",  // ✅ 直接改位置
      className
    )}
    {...props}
  >
    <X className="h-4 w-4" />
  </DialogPrimitive.Close>
))

优势:AI 直接改源码,因为组件就在你的项目里,在 components/ui/ 目录下。


📊 两种方案对比


📖 Section 1:传统组件库的 AI 困境

1.1 npm 黑盒依赖架构

为什么 AI 看不到源码?

问题原因影响
Token 限制node_modules 太大,AI 无法全部读取无法理解组件实现
编译后的代码只有编译后的 JS,不是源码代码可读性差
类型分离TypeScript 类型定义和实现分离无法完整理解 API

1.2 过度封装问题

示例:Ant Design Button 的内部实现(简化版)

jsx
// node_modules/antd/es/button/button.js
function Button({ loading, children, ...props }) {
  return (
    <button {...props}>
      {loading && <LoadingIcon />}  {/* 内部组件,你改不了 */}
      {children}
    </button>
  )
}

1.3 版本锁定困境

1.4 传统组件库问题总结表

维度传统组件库(Ant Design / MUI)AI 的困境
代码位置node_modules(黑盒)AI 看不到源码,只能靠文档
定制能力通过 props + CSS 覆盖AI 只能写脆弱的 CSS hack
版本升级Breaking Changes 多AI 难以评估影响范围
Bundle 大小全量引入无法按需精简
样式冲突全局 CSSAI 难以调试样式优先级
学习成本需要学习组件库 APIAI 需要大量文档上下文

📖 Section 2:shadcn/ui 的 Open Code 哲学

官方定义:shadcn/ui is a set of beautifully-designed, accessible components and a code distribution platform. This is not a component library. It is how you build your component library.

2.1 五大核心原则(官方)

原则说明实际意义
Open Code组件代码顶层开放,可直接修改不是"覆盖",而是"拥有"代码
Composition所有组件共享通用的可组合接口对团队和 AI 都可预测
DistributionSchema 定义 + CLI 分发跨项目、跨框架分发组件
Beautiful Defaults精心设计的默认样式开箱即用,风格统一
AI-Ready代码对 LLM 可读、可理解、可改进AI 可以学习并生成新组件

2.2 核心理念:代码在你手里

执行 npx shadcn@latest add button 后的项目结构:

css
components/
  ui/
    button.tsx  ← 完整的源码,在你的项目里,你可以随意修改

2.3 Button 组件源码解析

tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

// 使用 CVA 定义样式变体
const buttonVariants = cva(
  // 基础样式
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
)

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)

export { Button, buttonVariants }

代码结构解析:

2.3 CLI 工作流

初始化配置

bash
npx shadcn@latest init

交互式配置:

vbnet
✔ Which style would you like to use? › New York
✔ Which color would you like to use as base color? › Zinc
✔ Do you want to use CSS variables for colors? › yes
✔ Where is your global CSS file? › app/globals.css
✔ Configure the import alias for components: › @/components

生成的文件结构:

vbnet
components/
  ui/              ← 组件目录(空的)
lib/
  utils.ts         ← cn 工具函数
components.json    ← 配置文件

components.json 配置文件:

json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

2.4 Registry 系统

Registry 数据示例:

json
{
  "name": "button",
  "type": "components:ui",
  "files": [
    {
      "name": "button.tsx",
      "content": "import * as React from \"react\"..."
    }
  ],
  "dependencies": ["@radix-ui/react-slot", "class-variance-authority"],
  "registryDependencies": []
}

2.5 为什么 AI 能读、能改、能理解

原因说明AI 的优势
源码在项目里不在 node_modulesAI 可以直接读取
代码结构清晰Tailwind + Radix + CVAAI 一眼看懂
修改成本低直接改 buttonVariantsAI 精确定位修改点
没有版本锁定代码在 Git 仓库AI 可帮助版本对比合并

📖 Section 3:核心技术深度解析

3.1 CVA (Class Variance Authority) 详解

📌 CVA 是什么:一个用于管理 Tailwind CSS 类名变体的工具库

完整示例解析:

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

// 1️⃣ 创建变体配置
const buttonVariants = cva(
  // 🟢 基础样式:所有变体都会应用这些样式
  [
    "inline-flex items-center justify-center",     // 布局
    "gap-2 whitespace-nowrap",                     // 间距
    "rounded-md text-sm font-medium",              // 外观
    "transition-colors",                            // 过渡
    "focus-visible:outline-none focus-visible:ring-1", // 焦点
    "disabled:pointer-events-none disabled:opacity-50", // 禁用
  ],
  {
    // 🟡 变体定义
    variants: {
      // 样式变体
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      // 尺寸变体
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    // 🟣 默认值
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

// 2️⃣ 类型推导
type ButtonVariants = VariantProps<typeof buttonVariants>
// 结果:{ variant?: "default" | "destructive" | ... ; size?: "default" | "sm" | ... }

// 3️⃣ 使用
buttonVariants({ variant: "destructive", size: "lg" })
// 输出:"inline-flex items-center ... bg-destructive ... h-10 rounded-md px-8"

CVA 的优势:

优势说明
类型安全自动推导 variant 和 size 的类型
可组合多个变体可以自由组合
可扩展轻松添加新的变体
AI 友好结构清晰,AI 容易理解和修改

3.2 cn() 工具函数详解

📌 cn() 是什么:一个用于合并 Tailwind 类名的工具函数

实现源码:

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

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

两个库的作用:

具体示例:

tsx
// ✅ 处理条件类名
cn("px-4", isActive && "bg-blue-500", disabled && "opacity-50")
// 结果:"px-4 bg-blue-500"(假设 isActive=true, disabled=false)

// ✅ 解决冲突:后者覆盖前者
cn("px-4 py-2", "px-6")
// 结果:"py-2 px-6"(不是 "px-4 py-2 px-6")

// ✅ 处理对象形式
cn({
  "bg-red-500": hasError,
  "bg-green-500": isSuccess,
})

// ✅ 处理数组
cn(["flex", "items-center"], className)

为什么需要 cn()?

问题没有 cn()有 cn()
类名冲突"px-4 px-6" 两个都生效"px-6" 后者生效
条件类名需要手动处理 false 值自动过滤
组件覆盖无法优雅覆盖样式className 可覆盖默认样式

3.3 asChild 和 Slot 模式详解

📌 asChild 是什么:允许组件将其属性和行为传递给子元素,而不是渲染额外的 DOM 节点

Slot 的实现原理:

tsx
import { Slot } from "@radix-ui/react-slot"

// Button 组件内部
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ asChild = false, ...props }, ref) => {
    // 根据 asChild 决定渲染什么
    const Comp = asChild ? Slot : "button"
    return <Comp ref={ref} {...props} />
  }
)

// Slot 的简化实现
function Slot({ children, ...props }) {
  if (React.isValidElement(children)) {
    return React.cloneElement(children, {
      ...props,           // 父组件的 props(如 className、onClick)
      ...children.props,  // 子组件的 props
    })
  }
  return null
}

实际应用场景:

tsx
// 场景 1:Button 作为链接
import Link from "next/link"

<Button asChild>
  <Link href="/dashboard">进入控制台</Link>
</Button>

// 场景 2:Dialog.Trigger 自定义触发器
<Dialog.Trigger asChild>
  <Button variant="destructive">删除</Button>
</Dialog.Trigger>

// 场景 3:DropdownMenu.Item 作为链接
<DropdownMenu.Item asChild>
  <a href="/settings">设置</a>
</DropdownMenu.Item>

3.4 Dialog 组件完整源码解析

让我们深入看一个复杂组件的完整实现

tsx
// components/ui/dialog.tsx
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"

// 🟢 直接导出 Radix 原语
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=open]:fade-in-0",
      // 关闭动画
      "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
      className
    )}
    {...props}
  />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

// 🟣 包装内容区域,包含 Overlay 和关闭按钮
const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    {/* 自动渲染遮罩层 */}
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        // 定位和尺寸
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg",
        "translate-x-[-50%] translate-y-[-50%]",
        // 外观
        "gap-4 border bg-background p-6 shadow-lg sm:rounded-lg",
        // 动画
        "duration-200",
        "data-[state=open]:animate-in data-[state=closed]:animate-out",
        "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
        "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
        "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
        "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
        className
      )}
      {...props}
    >
      {children}
      {/* 内置关闭按钮 */}
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

// 🟠 便利组件:Header 和 Footer
const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-1.5 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
DialogHeader.displayName = "DialogHeader"

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
DialogFooter.displayName = "DialogFooter"

// 🟤 包装 Title 和 Description
const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      "text-lg font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

const DialogDescription = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

export {
  Dialog,
  DialogPortal,
  DialogOverlay,
  DialogClose,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
}

组件结构解析:

3.5 主题系统:CSS 变量配置

shadcn/ui 使用 CSS 变量实现主题系统,支持亮色/暗色模式

globals.css 配置示例:

css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    /* 🌞 亮色模式 */
    --background: 0 0% 100%;           /* 白色背景 */
    --foreground: 222.2 84% 4.9%;      /* 深色文字 */
    
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    
    --primary: 222.2 47.4% 11.2%;      /* 主色 */
    --primary-foreground: 210 40% 98%;
    
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    
    --destructive: 0 84.2% 60.2%;      /* 危险色 */
    --destructive-foreground: 210 40% 98%;
    
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    
    --radius: 0.5rem;                   /* 圆角 */
  }

  .dark {
    /* 🌙 暗色模式 */
    --background: 222.2 84% 4.9%;       /* 深色背景 */
    --foreground: 210 40% 98%;          /* 浅色文字 */
    
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    
    /* ... 其他变量 ... */
  }
}

在组件中使用:

tsx
// 使用 CSS 变量
className="bg-primary text-primary-foreground"
// 等价于
className="bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]"

主题切换实现:

tsx
import { useTheme } from "next-themes"

function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  
  return (
    <Button
      variant="outline"
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
    >
      {theme === "dark" ? "🌞" : "🌙"}
    </Button>
  )
}

📖 Section 4:shadcn/ui 生态工具

3.1 先建立生态地图:这些工具分别解决什么问题

很多人接触 shadcn/ui 生态时,会有一种“工具很多,但分不清谁负责什么”的感觉。

我建议大家不要把它们都当成“组件库”,而是分成四层来理解:

text
第 1 层:基础层
  shadcn/ui
  负责:可控、可改、可复制的基础组件

第 2 层:发现与分发层
  21st.dev / Registry / 自定义 Registry
  负责:找组件、复用组件、分发组件

第 3 层:视觉增强层
  magic-ui / aceternity-ui
  负责:动效、视觉冲击、营销感

第 4 层:AI 加速层
  TweakCN / v0.dev
  负责:主题探索、代码生成

这套划分非常适合教学。因为它告诉学员:

  • shadcn/ui 不是一个孤立产品
  • 它已经形成了围绕“Open Code + Copy-Paste”展开的完整生态
  • 这些工具不是互相替代,而是分工协作

3.2 21st.dev:组件发现与生态入口

🔗 官网:https://21st.dev

如果说 shadcn/ui 解决的是“组件怎么进项目”,那 21st.dev 解决的是“我去哪里找值得拿来用的组件”。

这类平台的重要性,在 AI 时代被明显放大了。

为什么?

因为过去我们找组件的方式通常是:

  • Google 搜博客
  • GitHub 上翻仓库
  • 复制别人的 demo
  • 自己再改一遍

这个流程不仅慢,而且质量参差不齐。

21st.dev 这类平台的价值在于,它把基于 shadcn/ui、Tailwind、现代 React 工作流的组件做了集中展示和发现。你可以把它理解为:

shadcn 生态的“组件搜索引擎 + 灵感库 + 分发入口”。

它适合什么场景

  • 你知道自己要做一个 UI 模块,但不想从零写
  • 你想找“更像现代 SaaS”的组件表达
  • 你需要给 AI 一个更明确的参考对象
  • 你希望团队先挑一个成熟模式,再落代码

正确的使用姿势

不要把 21st.dev 当成“复制粘贴网站”,而应该把它当成:

  1. 参考源 先看别人是怎么组织结构、命名和交互的

  2. 模式库 你不是只拿一个按钮,而是在学一个“仪表盘卡片”“命令面板”“pricing section”的成熟模式

  3. AI 上下文增强器 当你让 Cursor 或 v0.dev 生成某个模块时,你可以先在 21st.dev 找到相似案例,再把结构语言描述得更准确

比如你想做一个 Command Menu,不要只说:

text
Create a command menu.

你可以先看生态里常见的实现,再写成:

text
Create a command menu similar to modern shadcn-style patterns:
search input on top, grouped actions, keyboard shortcut hints,
hover and active states, compact spacing, dark-mode friendly.

这种 Prompt 的质量会高很多。

它和 Registry 的关系

这里也要讲清楚,避免大家混淆:

  • Registry 更偏“分发机制”
  • 21st.dev 更偏“发现入口”

前者解决“怎么拉到项目里”,后者解决“我先找到什么值得拉”。

3.3 magic-ui:动画组件库

🔗 官网:https://magicui.design

特点: 专注于炫酷的动画效果,完全兼容 shadcn/ui 工作流

如果你觉得 shadcn/ui 默认组件“很好用,但有点素”,那 magic-ui 就是在补这一块。

它最适合的不是后台 CRUD,而是:

  • Landing Page
  • Hero 区
  • 产品能力展示
  • 数据流、流程感、品牌氛围强化

换句话说,magic-ui 解决的是“组件已经够用了,但页面还不够有记忆点”的问题。

为什么它在生态里重要

因为很多团队会陷入一个误区:

  • shadcn/ui 很稳
  • 但页面容易“像后台模板”

这时你不一定需要换组件库,你更需要的是少量高质量的视觉增强组件magic-ui 非常适合扮演这个角色。

安装方式:

bash
npx shadcn@latest add "https://magicui.design/r/marquee"

示例组件:

组件效果适用场景
Marquee跑马灯品牌展示、合作伙伴
Animated Beam连线动画流程展示、架构图
Particles粒子效果背景装饰、氛围营造
tsx
// Marquee 示例
import Marquee from "@/components/magicui/marquee"

export function MarqueeDemo() {
  return (
    <Marquee pauseOnHover className="[--duration:20s]">
      <div>Item 1</div>
      <div>Item 2</div>
      <div>Item 3</div>
    </Marquee>
  )
}

使用建议

我建议课堂上把 magic-ui 定位成“加分层”,而不是“基础层”:

  • 基础交互先用 shadcn/ui
  • 真正需要视觉亮点的局部,再引入 magic-ui

否则很容易把页面做得过度动画化,最后变成“看起来很酷,但不耐用”。

3.4 aceternity-ui:现代 UI 组件

🔗 官网:https://ui.aceternity.com

特点: 3D 效果、视觉冲击,适合营销页面、落地页

如果说 magic-ui 更偏“动效增强”,那 aceternity-ui 更偏“视觉表达升级”。

它的特点不是“更标准”,而是“更抓眼球”:

  • 更强的空间感
  • 更明显的品牌感
  • 更适合宣传页、概念展示、产品首页

它不太适合做什么?

  • 企业后台主工作流
  • 信息密度很高的业务表单
  • 要求强一致、强可维护的中后台页面

也就是说,它特别适合讲给学员一个判断标准:

不是所有好看的组件都适合放进业务核心路径。

生态工具的关键,不只是会不会用,而是知道该放在哪里用。

示例组件:

组件效果适用场景
3D Card3D 悬浮卡片产品展示
Background Beams光束背景Hero 区域
Spotlight聚光灯效果重点内容
tsx
// 3D Card 示例
import { CardContainer, CardBody, CardItem } from "@/components/ui/3d-card"

export function ThreeDCardDemo() {
  return (
    <CardContainer className="inter-var">
      <CardBody>
        <CardItem translateZ="50">
          <h3>3D Card</h3>
        </CardItem>
        <CardItem translateZ="100">
          <img src="/image.jpg" />
        </CardItem>
      </CardBody>
    </CardContainer>
  )
}

如何跟 shadcn/ui 结合

最好的用法通常不是“全站都用 aceternity-ui”,而是:

  • shadcn/ui 负责信息结构和交互骨架
  • aceternity-ui 负责首页、空状态、品牌展示区域的视觉高光

这样既能保证工程稳定性,又能有足够强的视觉辨识度。

3.5 TweakCN:从“换主题”升级到“主题工作流”

🔗 GitHub:https://github.com/tweakcn/tweakcn

很多同学第一次看到 TweakCN,会把它理解成“一个 shadcn/ui 配色生成器”。这也太低估它了。

如果从课程视角来讲,TweakCN 最重要的价值不是“帮你挑颜色”,而是它把下面这件事做得非常简单:

把抽象的品牌感觉,快速落成一套可运行的 CSS Variables 主题。

这件事为什么重要?

因为在 shadcn/ui 工作流里,组件本身只是骨架,真正决定整体风格的是:

  • --background
  • --foreground
  • --primary
  • --secondary
  • --accent
  • --muted
  • --border
  • --ring
  • --radius

也就是说,shadcn/ui 的生态不是“找一堆组件来拼”,而是:

text
组件源码 + Radix 原语 + Tailwind utility + CSS Variables 主题

TweakCN 刚好卡在这个链路的关键位置上。它不是替代组件库,而是帮助你更快地完成主题系统设计

使用示例:

less
输入: "Create a dark theme with purple as primary color, 
      pink as accent color, and a subtle gradient background"

AI 生成:

css
:root {
  --background: 224 71% 4%;
  --foreground: 213 31% 91%;
  --primary: 263 70% 50%;
  --primary-foreground: 210 40% 98%;
  --accent: 330 81% 60%;
  --accent-foreground: 222 47% 11%;
}

它到底解决了什么问题

传统主题定制的问题是这样的:

  1. 设计师说“我想要更科技感一点、偏紫色、暗一点”
  2. 开发者打开 globals.css
  3. 一条条改 HSL 变量
  4. 改完刷新页面
  5. 再回去继续试

这个过程的问题不是“麻烦”,而是没有反馈闭环。你改的是变量,不是结果;你看到的是结果,但不知道该回到哪个变量继续调。

TweakCN 的优势就在于:

  • 它把“主题变量”和“组件预览”放到一起
  • 它让你可以从视觉结果反推变量组合
  • 它降低了非设计师、非资深前端调主题的门槛

所以它适合的不是“设计系统专家”,而是:

  • 想快速试品牌风格的前端
  • 想做白标/多品牌产品的团队
  • 想用 AI 辅助主题探索的开发者

怎么把 TweakCN 正确用到项目里

我建议大家用下面这套流程,而不是“生成一份主题就直接贴进去”。

第一步:先定语义,不先定颜色

不要一上来就说“我要一个紫色主题”,而是先明确:

  • 主品牌色是什么
  • 主要强调操作是什么
  • 页面背景是偏冷还是偏暖
  • 卡片要不要和页面背景拉开层次
  • 暗色模式是纯黑路线还是深灰路线

这是因为 TweakCN 输出的是变量值,但你要先想清楚变量背后的语义。

第二步:用自然语言快速探索

例如:

text
Create a dark B2B dashboard theme with indigo primary,
subtle violet accent, low-saturation background,
clear card hierarchy, and accessible contrast.

这一步的目标不是“一次成功”,而是快速试 3 到 5 轮,找到比较接近的方向。

第三步:把输出映射回 Design Token 体系

很多团队这里会犯一个错:把 TweakCN 输出直接当最终答案。

正确做法应该是:

  • 看它生成的 --primary--accent--muted 是否符合品牌
  • --border--ring 是否满足交互和焦点态需求
  • 看亮暗模式下语义是否稳定
  • 把最终确认的变量纳入你自己的 Token 文档

也就是说,TweakCN 更像“探索器”,而不是“真理机”。

实际接入步骤

最常见的落地方式是:

  1. TweakCN 中生成主题
  2. 复制变量配置
  3. 放进项目的 app/globals.css 或主题文件
  4. 用现有的 shadcn/ui 组件直接预览
  5. 再做局部微调

示例:

css
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 224 71% 4%;
    --card: 0 0% 100%;
    --card-foreground: 224 71% 4%;
    --primary: 262 83% 58%;
    --primary-foreground: 210 40% 98%;
    --accent: 270 95% 75%;
    --accent-foreground: 224 71% 4%;
    --border: 220 13% 91%;
    --ring: 262 83% 58%;
    --radius: 0.75rem;
  }
}

然后你原来的按钮、卡片、输入框,不改组件源码,视觉就会整体变化:

tsx
<Button>保存设置</Button>
<Card>
  <CardHeader>
    <CardTitle>团队空间</CardTitle>
  </CardHeader>
</Card>

这就是它最强的地方:

不需要重写组件,只需要重写主题变量。

TweakCN 和 v0.dev、Cursor、Design Token 的关系

这里一定要讲透,因为这正是 AI 时代它的价值所在。

TweakCN 不负责生成业务页面,v0.dev 才负责; TweakCN 也不负责管理所有设计资产,设计系统和 Token 文档才负责。

它更像是中间层:

text
品牌/设计感觉
   ↓
TweakCN 快速探索主题变量
   ↓
沉淀为 Design Token / CSS Variables
   ↓
v0.dev / Cursor / 本地开发继续生成和修改组件

所以它最适合的使用时机有三个:

  1. 项目初始化时:快速找到第一版主题
  2. 品牌升级时:低成本试多套视觉方向
  3. AI 生成代码前:先把主题基线定好,减少后续硬编码颜色

这类生态工具怎么分类理解

你刚看到 TweakCN 的时候,可能会觉得它和 magic-uiv0.dev 都是“生态工具”。但它们解决的问题完全不同:

工具类型代表工具解决的问题
主题探索工具TweakCN生成和调试主题变量
视觉增强组件库magic-ui / aceternity-ui提供炫酷组件和动效
代码生成工具v0.dev生成页面和组件代码
设计系统工具shadcn/ui registry分发和维护组件源码

这个分类很重要。否则团队很容易把所有“AI UI 工具”混成一类,结果是:

  • 该用 TweakCN 的时候去问 v0.dev 配色
  • 该用设计 Token 的时候直接写死颜色
  • 该统一主题的时候却在一个个组件里微调 className

使用建议

最后给大家几个非常实用的建议:

  1. 先定主题,再批量生成页面 先用 TweakCN 把变量体系定下来,再让 AI 生成页面,整体风格会稳很多。

  2. 保留一份“品牌基线主题” 不要每次都从零试,把你们确认过的主题版本沉淀到代码库和文档里。

  3. 不要把 TweakCN 输出原封不动上线 一定要结合无障碍对比度、品牌规范和业务场景再 review 一遍。

  4. 把它当探索工具,不要当设计系统本身 它擅长生成方向,不擅长替你做长期治理。

3.6 v0.dev:AI 代码生成

🔗 官网:https://v0.dev(Vercel 出品)

使用示例:

less
输入: "Create a user profile card with avatar, name, email, and edit button"

v0.dev 生成:

tsx
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"

export function UserProfileCard() {
  return (
    <Card className="w-full max-w-md">
      <CardContent className="flex items-center gap-4 p-6">
        <Avatar className="h-16 w-16">
          <AvatarImage src="/avatar.jpg" />
          <AvatarFallback>JD</AvatarFallback>
        </Avatar>
        <div className="flex-1">
          <h3 className="font-semibold text-lg">John Doe</h3>
          <p className="text-sm text-muted-foreground">john@example.com</p>
        </div>
        <Button variant="outline" size="sm">Edit</Button>
      </CardContent>
    </Card>
  )
}

3.7 生态工具对比表

工具定位特点使用场景GitHub Stars
shadcn/ui基础组件库Copy-Paste 哲学所有项目80K+
21st.dev组件发现平台找模式、找灵感、找实现组件探索、AI 上下文增强平台型产品
magic-ui动画组件库炫酷动画效果营销页面、落地页10K+
aceternity-ui现代 UI 组件3D 效果、视觉冲击创意项目、展示页15K+
TweakCNAI 主题编辑器自然语言生成主题快速定制主题GitHub 项目
v0.devAI 代码生成描述生成组件快速原型开发Vercel 产品

💡 课堂上最好这样总结shadcn/ui 提供骨架,21st.dev 帮你找模式,magic-ui/aceternity-ui 帮你做视觉增强,TweakCN 帮你定主题,v0.dev 帮你快速生成页面。


📖 Section 5:横向对比组件库

5.1 对比维度

5.2 主流组件库对比

维度Ant DesignMUIChakra UIshadcn/uiHeadless UI
分发方式npm 包npm 包npm 包Copy-Pastenpm 包
定制能力主题配置主题配置主题配置源码修改完全自定义
AI 友好性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
样式方案LessEmotionEmotionTailwind无样式
无障碍性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Bundle 大小~500KB~400KB~200KB按需~50KB
学习成本

5.3 AI 友好性详细对比

5.4 选型建议

场景推荐方案理由
新项目,追求 AI 效率shadcn/ui + TailwindAI 友好性最高
传统中后台项目Ant Design组件丰富,开箱即用
需要 Material DesignMUI设计规范完整
Vue + Tailwind 项目Headless UI官方支持 Vue
完全自定义设计Radix UI + Tailwind最大灵活性

📖 Section 6:实战演示

6.1 从零初始化项目

Step 1: 创建 Next.js 项目

bash
npx create-next-app@latest my-app
cd my-app

选择配置:

css
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like to use App Router? Yes

Step 2: 初始化 shadcn/ui

bash
npx shadcn@latest init

Step 3: 添加组件

bash
npx shadcn@latest add button card dialog

项目结构:

css
my-app/
├── app/
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   └── ui/
│       ├── button.tsx    ← 源码在这里
│       ├── card.tsx      ← 源码在这里
│       └── dialog.tsx    ← 源码在这里
├── lib/
│   └── utils.ts
└── components.json

Step 4: 使用组件

tsx
// app/page.tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"

export default function Home() {
  return (
    <main className="flex min-h-screen items-center justify-center p-24">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>欢迎使用 shadcn/ui</CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <p className="text-muted-foreground">
            这是一个基于 Copy-Paste 哲学的组件库。
          </p>
          <Button className="w-full">开始使用</Button>
        </CardContent>
      </Card>
    </main>
  )
}

6.2 用 AI 定制组件

需求:给 Button 加一个渐变变体

告诉 AI: "帮我给 Button 组件加一个 gradient 变体,从紫色渐变到粉色。"

AI 修改 components/ui/button.tsx

tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center ...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground ...",
        // ✅ AI 添加这一行
        gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg hover:shadow-xl hover:from-purple-600 hover:to-pink-600",
      },
      // ...
    },
  }
)

使用:

tsx
<Button variant="gradient">渐变按钮</Button>

⏱️ 整个过程不到 10 秒

6.3 实战技巧清单

技巧命令说明
批量添加组件npx shadcn@latest add button card dialog input一次添加多个
查看可用组件npx shadcn@latest add列出所有组件
更新组件npx shadcn@latest add button --overwrite覆盖更新
对比差异git diff components/ui/button.tsx更新前先对比

6.4 组件变体库目录建议

bash
components/
  ui/              ← shadcn/ui 原始组件
  variants/        ← 自定义变体
    gradient-button.tsx
    glass-card.tsx

💡 既保留原始组件,又有自定义版本

6.5 创建自定义 Registry

如果你的团队有自己的设计系统,可以创建自己的 Registry

Step 1:创建 Registry 服务器

json
// https://your-registry.com/r/custom-button.json
{
  "name": "custom-button",
  "type": "components:ui",
  "files": [
    {
      "name": "custom-button.tsx",
      "content": "import * as React from \"react\"\n\nexport function CustomButton({ children, gradient = false, ...props }) {\n  return (\n    <button\n      className={cn(\n        'px-4 py-2 rounded-lg font-medium',\n        gradient && 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  )\n}"
    }
  ],
  "dependencies": [],
  "registryDependencies": ["button"]
}

Step 2:配置 components.json

json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  },
  "registries": {
    "my-team": {
      "url": "https://your-registry.com/r"
    }
  }
}

Step 3:使用自定义组件

bash
# 从自定义 Registry 添加组件
npx shadcn@latest add my-team/custom-button

# 或者直接使用 URL
npx shadcn@latest add "https://your-registry.com/r/custom-button"

6.6 常见问题 FAQ

Q1:shadcn/ui 适合所有项目吗?

项目类型是否适合说明
新项目✅ 非常适合从零开始,完全控制
中小型项目✅ 适合灵活定制,效率高
大型企业项目✅ 适合配合自定义 Registry
严格 Material Design⚠️ 不太适合MUI 更合适
已有 Ant Design 的项目⚠️ 渐进式可并存,逐步迁移

Q2:Copy-Paste 模式会不会导致代码难以维护?

不会。原因:

  1. 组件在你的 Git 仓库,可以用 Git 管理版本
  2. shadcn/ui 的组件都很小,单文件,易于维护
  3. 代码结构清晰,标准化程度高
  4. AI 可以帮助理解和修改

Q3:如何在现有项目中引入 shadcn/ui?

bash
# 1. 初始化(不会影响现有代码)
npx shadcn@latest init

# 2. 逐步添加组件(按需)
npx shadcn@latest add button dialog

# 3. 在新功能中使用
import { Button } from "@/components/ui/button"

💡 建议:老组件和新组件可以并存,逐步迁移

Q4:shadcn/ui 的性能如何?

指标shadcn/uiAnt DesignMUI
Bundle Size按需加载~500KB~400KB
运行时性能极佳良好良好
样式计算无运行时开销较少CSS-in-JS 开销
Tree Shaking完美支持部分支持部分支持

Q5:shadcn/ui 支持哪些框架?

框架支持程度说明
Next.js✅ 官方支持App Router 和 Pages Router
Remix✅ 官方支持完整支持
Vite + React✅ 官方支持最简单的配置
Astro✅ 官方支持需要 React 集成
Laravel✅ 官方支持Inertia.js
Gatsby⚠️ 社区支持需要手动配置

Q6:如何升级 shadcn/ui 的组件?

bash
# 方法 1:直接覆盖(丢失自定义修改)
npx shadcn@latest add button --overwrite

# 方法 2:先对比再决定(推荐)
git stash                                    # 保存当前修改
npx shadcn@latest add button --overwrite     # 拉取最新版本
git diff components/ui/button.tsx           # 对比差异
git stash pop                                # 恢复修改
# 手动合并差异

Q7:可以用 shadcn/ui 做移动端吗?

可以,但需要注意:

  1. 响应式设计:默认桌面端优先,需要调整
  2. 触摸交互:部分组件需要适配移动端手势
  3. 底部抽屉:可以配合 Vaul 库实现
tsx
// 移动端底部抽屉示例
import { Drawer } from "vaul"

<Drawer.Root>
  <Drawer.Trigger asChild>
    <Button>打开菜单</Button>
  </Drawer.Trigger>
  <Drawer.Content>
    {/* 移动端友好的内容 */}
  </Drawer.Content>
</Drawer.Root>

6.7 2025 新特性:Skills 和 MCP Server

💡 最新更新:shadcn/ui 已全面拥抱 AI 原生开发,新增 Skills 和 MCP Server 功能。

Skills:给 AI 助手赋能

安装 Skills:

bash
pnpm dlx skills add shadcn/ui

使用场景示例:

PromptAI 行为
"Add a login form with email and password"使用 Field、Input、Button 组件
"Create a settings page"使用 Tabs、Card、Form 组件
"Build a dashboard with sidebar"使用 Sidebar、Chart、Table 组件

MCP Server:AI 直接访问注册表

MCP (Model Context Protocol) 是 AI 助手安全连接外部工具的开放协议。

配置 MCP(Claude Code):

json
// .mcp.json
{
  "mcpServers": {
    "shadcn": {
      "command": "npx",
      "args": ["shadcn@latest", "mcp"]
    }
  }
}

自然语言使用示例:

css
# 你可以直接对 AI 说:
"Show me all available components in the shadcn registry"
"Add the button, dialog and card components to my project"
"Find me a login form from the shadcn registry"
"Build a landing page using components from the acme registry"

6.8 October 2025 新组件

最新组件:Field、Item、Button Group、Input Group、Spinner、Kbd、Empty

Field 组件:表单新范式

tsx
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field"

<Field>
  <FieldLabel htmlFor="username">Username</FieldLabel>
  <Input id="username" placeholder="Max Leiter" />
  <FieldDescription>Choose a unique username.</FieldDescription>
  <FieldError>Username is already taken.</FieldError>
</Field>

Field 支持的表单库:

表单库支持说明
React Hook Form完整集成
TanStack Form完整集成
Server Actions原生支持
自定义灵活适配

Input Group:输入框组合

tsx
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"

<InputGroup>
  <InputGroupAddon>https://</InputGroupAddon>
  <InputGroupInput placeholder="your-domain" />
  <InputGroupAddon>.com</InputGroupAddon>
</InputGroup>

Item 组件:列表项抽象

tsx
import { Item, ItemContent, ItemTitle, ItemDescription, ItemMedia } from "@/components/ui/item"

<Item>
  <ItemMedia variant="icon">
    <HomeIcon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Dashboard</ItemTitle>
    <ItemDescription>Overview of your account.</ItemDescription>
  </ItemContent>
</Item>

📖 Section 7:深度思考

7.1 范式转移:从依赖到拥有

7.2 AI 时代组件库设计五原则

7.3 Copy-Paste 哲学的局限性

局限说明解决方案
代码冗余多项目各有一份代码每个项目需求可能不同,冗余=灵活
更新成本需手动同步更新可选择性更新,不被强制升级
团队协作各自修改可能不一致创建自定义 Registry 或用 Git 管理
学习曲线新手不习惯理解好处后会爱上

📋 Closing:总结与行动建议

核心要点速查

✅ 行动建议清单

新项目

  • 使用 Tailwind CSS v4 作为样式方案
  • 使用 shadcn/ui 作为组件库
  • 配合 v0.dev + Cursor 进行 AI 辅助开发

老项目

  • 渐进式迁移:先用 shadcn/ui 做新功能
  • 混合使用:shadcn/ui + 老组件库并存
  • 评估成本:稳定项目不一定要迁移

📋 知识点速查表

概念定义关键点
Copy-Paste 哲学组件源码复制到项目中代码即资产,拥有即控制
CLI 工作流npx shadcn@latest add从 Registry 拉取源码
Registry组件的中央仓库JSON API,存储元数据和源码
CVAClass Variance Authority管理 Tailwind 变体
cn()类名合并工具clsx + tailwind-merge
asChildRadix Slot 模式避免额外 DOM 节点

📚 下节课预告

第 3 课:Radix UI - 无头组件的底层逻辑

  • Headless UI 的设计哲学
  • Radix UI 的 Composition 模式
  • 可访问性原语
  • shadcn/ui 如何基于 Radix 构建

课程时间分配:

部分时长
Opening: 现场对比演示10 min
Section 1: 传统组件库的 AI 困境20 min
Section 2: Copy-Paste 哲学30 min
Section 3: 生态工具25 min
Section 4: 横向对比20 min
Section 5: 实战演示20 min
Section 6: 深度思考15 min
Closing + Q&A10 min
总计2.5 小时