第三章:Tailwind CSS + shadcn/ui——高效 UI 开发
第三章:Tailwind CSS + shadcn/ui——高效 UI 开发
shadcn/ui 不是一个组件库,而是一个"组件源码集合"。你把源码复制进项目,然后完全拥有它——可以随意修改,不被任何第三方库版本锁死。配合 Tailwind CSS,是 2024 年最主流的 SaaS UI 开发方式。
一、Tailwind CSS 核心哲学
<!-- 传统 CSS:写 class 然后写样式 -->
<button class="btn-primary">提交</button>
/* CSS */
.btn-primary {
background-color: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 6px;
}
<!-- Tailwind:直接在 class 中写样式(Utility-first) -->
<button class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors">
提交
</button>
为什么 Tailwind 更适合 SaaS 开发:
- 不用在 HTML 和 CSS 文件之间来回切换
- 样式和组件强绑定,删组件不会留 CSS “孤儿”
- 响应式前缀(
md:,lg:)让布局更直观 - Dark mode 前缀(
dark:)一行搞定暗黑模式
// 响应式示例
<div className="
grid
grid-cols-1 // 手机:1列
md:grid-cols-2 // 平板:2列
lg:grid-cols-3 // 桌面:3列
gap-4
">
{/* 内容 */}
</div>
// 暗黑模式示例
<div className="
bg-white text-gray-900 // 亮色模式
dark:bg-gray-900 dark:text-white // 暗色模式
p-6 rounded-lg
">
二、shadcn/ui 安装与使用
# 初始化(在 Next.js 项目中)
npx shadcn-ui@latest init
# 添加组件(只下载你需要的)
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add form
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add table
npx shadcn-ui@latest add toast
# 组件会下载到 components/ui/ 目录
# 你完全拥有这些代码,可以随意修改!
// components/ui/button.tsx(shadcn/ui 生成,可修改)
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
// 基础样式(所有按钮共有)
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
三、Landing Page 实战
// app/(marketing)/page.tsx - Landing Page
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Check, Zap, Shield, BarChart3 } from 'lucide-react'
// Hero Section
function Hero() {
return (
<section className="py-24 px-4 text-center max-w-4xl mx-auto">
<Badge variant="outline" className="mb-4">
🚀 刚刚发布 v2.0
</Badge>
<h1 className="text-5xl font-bold tracking-tight text-gray-900 dark:text-white mb-6">
用 AI 提升你的
<span className="text-blue-600"> 工作效率</span>
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
面向开发者的 AI 工具平台。从代码审查到文档生成,
让 AI 成为你最可靠的工作伙伴。
</p>
<div className="flex gap-4 justify-center">
<Button size="lg" asChild>
<Link href="/register">免费开始使用</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="#demo">查看演示</Link>
</Button>
</div>
{/* 信任信号 */}
<p className="text-sm text-gray-500 mt-4">
已有 2,000+ 开发者使用 · 不需要信用卡
</p>
</section>
)
}
// Features Section
const features = [
{
icon: Zap,
title: "极速响应",
description: "平均响应时间 < 200ms,不让 AI 拖慢你的工作流",
},
{
icon: Shield,
title: "数据安全",
description: "你的代码永远不会被用来训练模型,SOC 2 认证",
},
{
icon: BarChart3,
title: "使用分析",
description: "详细的 token 使用统计,优化你的 AI 成本",
},
]
function Features() {
return (
<section className="py-24 bg-gray-50 dark:bg-gray-900">
<div className="max-w-6xl mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">
为开发者设计的每一个细节
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature) => (
<div
key={feature.title}
className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border"
>
<feature.icon className="h-8 w-8 text-blue-600 mb-4" />
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
<p className="text-gray-600 dark:text-gray-400">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
)
}
// Pricing Section
const plans = [
{
name: "Free",
price: "$0",
period: "/月",
description: "适合个人探索",
features: ["100,000 tokens/月", "5 个项目", "邮件支持"],
cta: "开始使用",
href: "/register",
highlighted: false,
},
{
name: "Pro",
price: "$29",
period: "/月",
description: "适合专业开发者",
features: ["无限 tokens", "无限项目", "优先支持", "API 访问", "团队共享"],
cta: "开始 14 天试用",
href: "/register?plan=pro",
highlighted: true, // 突出显示
},
{
name: "Team",
price: "$99",
period: "/月",
description: "适合开发团队",
features: ["10 个团队成员", "SSO 支持", "审计日志", "专属支持", "SLA 保证"],
cta: "联系我们",
href: "/contact",
highlighted: false,
},
]
function Pricing() {
return (
<section className="py-24 px-4" id="pricing">
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-4">简单透明的定价</h2>
<p className="text-center text-gray-600 mb-12">随时可以升级或取消</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{plans.map((plan) => (
<div
key={plan.name}
className={`rounded-xl p-8 border ${
plan.highlighted
? 'border-blue-600 bg-blue-50 dark:bg-blue-950 ring-2 ring-blue-600'
: 'border-gray-200 dark:border-gray-700'
}`}
>
{plan.highlighted && (
<Badge className="mb-4 bg-blue-600">最受欢迎</Badge>
)}
<h3 className="text-xl font-bold">{plan.name}</h3>
<div className="mt-2">
<span className="text-4xl font-bold">{plan.price}</span>
<span className="text-gray-600">{plan.period}</span>
</div>
<p className="text-gray-600 mt-2 mb-6">{plan.description}</p>
<ul className="space-y-3 mb-8">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<Check className="h-4 w-4 text-green-600 flex-shrink-0" />
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
<Button
className="w-full"
variant={plan.highlighted ? "default" : "outline"}
asChild
>
<Link href={plan.href}>{plan.cta}</Link>
</Button>
</div>
))}
</div>
</div>
</section>
)
}
// 主页面
export default function LandingPage() {
return (
<main>
<Hero />
<Features />
<Pricing />
</main>
)
}
四、Dashboard 布局
// app/(dashboard)/layout.tsx
import Link from 'next/link'
import { LayoutDashboard, Settings, CreditCard, Code } from 'lucide-react'
import { getCurrentUser } from '@/lib/auth'
import { UserMenu } from '@/components/user-menu'
const navItems = [
{ href: '/dashboard', icon: LayoutDashboard, label: '概览' },
{ href: '/dashboard/projects', icon: Code, label: '项目' },
{ href: '/dashboard/billing', icon: CreditCard, label: '账单' },
{ href: '/dashboard/settings', icon: Settings, label: '设置' },
]
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getCurrentUser()
return (
<div className="flex h-screen bg-gray-100 dark:bg-gray-900">
{/* 侧边栏 */}
<aside className="w-64 bg-white dark:bg-gray-800 border-r flex flex-col">
{/* Logo */}
<div className="p-6 border-b">
<Link href="/dashboard" className="text-xl font-bold text-blue-600">
YourSaaS
</Link>
</div>
{/* 导航 */}
<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<item.icon className="h-5 w-5" />
{item.label}
</Link>
))}
</nav>
{/* 用户信息 */}
<div className="p-4 border-t">
<UserMenu user={user} />
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 overflow-auto p-8">
{children}
</main>
</div>
)
}
五、暗黑模式实现
# 安装 next-themes
npm install next-themes
// components/theme-provider.tsx
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({ children, ...props }: any) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
// components/theme-toggle.tsx
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
)
}
关键认知
shadcn/ui 的哲学比技术本身更重要:
传统组件库(Ant Design / MUI):组件代码在 node_modules,你只是调用者,遇到奇怪 bug 或需要深度定制时,要么 hack CSS,要么 Fork 项目。
shadcn/ui:组件代码直接在你的项目里,你是所有者。版本升级是主动选择,不会被动被 breaking change 影响。
Landing Page 转化率核心要素:
- 标题说清楚"对谁有什么价值"(< 10 字)
- 副标题说清楚"怎么实现"(< 30 字)
- 两个 CTA:主要(立即开始)+ 次要(了解更多)
- 社会证明(用户数量/公司 Logo)放在 Fold 以上
“UI 是产品的第一印象。用 shadcn/ui + Tailwind,你可以在 1 天内搭出看起来专业的 SaaS 界面。剩下的 99% 价值来自产品功能和用户体验,而不是 UI 库的选择。”