Claude Code 使用 Tailwind CSS 的实战技巧:稳定 UI 改造指南
用 Claude Code 和 Tailwind CSS 改造 UI:设计令牌、响应式、深色模式、组件抽取、safelist 与视觉检查。
Tailwind CSS 的优点是可以用 p-4、grid、text-sm 这样的 utility class 快速搭 UI。Claude Code 又能读取代码库、修改文件、运行命令、做多文件重构,所以两者配合非常适合做前端改善。但如果只说“帮我美化一下”,结果往往会变成很长的 className、颜色不统一、移动端错位、深色模式漏掉文字颜色,或者生产环境里某些动态 class 没有生成 CSS。
这篇文章面向刚开始把 Claude Code 用到 Tailwind 项目的开发者。我们会从设计令牌开始,逐步讲响应式工具类、组件抽取、避免 class soup、深色模式、表单/按钮/卡片、safelist 与 content scanning,以及用 Playwright 做视觉检查。示例使用 React + TypeScript,但这些检查方法同样适用于 Astro、Next.js、Remix、Vite 等项目。
本文参考 Tailwind 官方文档的 Theme variables、Responsive design、Dark mode、Detecting classes in source files、Adding custom styles。React 代码参考 React TypeScript 指南,Claude Code 的基本能力参考 Claude Code 官方文档,视觉回归参考 Playwright Visual comparisons。
如果你还不确定怎样写需求,可以先看更好的提示词技巧。如果改造目标是完整移动端体验,也可以接着看 PWA 开发指南。
先让 Claude Code 做 UI 盘点
不要一开始就让 Claude Code 改文件。Tailwind 的问题通常不是单个按钮,而是颜色、间距、断点、深色模式和 CTA 流程之间不一致。先让它读取现有结构并报告风险。
请检查这个仓库中 Tailwind CSS 的使用情况,先不要修改文件。
请按以下项目输出报告:
- 已存在的颜色、间距、圆角、阴影、字体设计令牌
- className 过长的 React 组件
- 375px、768px、1440px 下可能会崩的布局
- 深色模式已经覆盖和未覆盖的位置
- 表单、按钮、卡片、badge 中重复的样式
- 可能因为动态 class 导致生产 CSS 缺失的代码
- 现有的 Playwright、Storybook 或浏览器检查方式
Masa 在 ClaudeCodeLab 做 UI 改善时踩过一个坑:只看桌面端文章卡片,结果移动端文章末尾的咨询 CTA 和广告区域间距太窄。Tailwind class 看起来是局部修改,但实际会影响整个转化路径。
用设计令牌统一颜色和间距
设计令牌就是把颜色、间距、字体、圆角、阴影等基础设计值命名。Tailwind CSS v4 采用 CSS-first 的 @theme,定义后可以直接得到 bg-brand-600、rounded-card 这样的工具类。旧项目如果仍使用 tailwind.config.ts,也可以把同样的值放在 theme.extend 中。
/* src/styles/app.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: Inter, "Noto Sans SC", system-ui, sans-serif;
--color-brand-50: #eef6ff;
--color-brand-100: #d9ebff;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-ink: #111827;
--color-muted: #6b7280;
--color-surface: #ffffff;
--color-danger: #dc2626;
--radius-card: 0.75rem;
--shadow-card: 0 16px 40px rgb(15 23 42 / 0.08);
}
给 Claude Code 的指令应当是:“优先使用已有 brand、surface、danger 等 token;只有出现可复用的新语义时才添加 @theme 变量。”不要说“换个好看的蓝色”,否则同一个站点里可能混用 blue、sky、indigo,以后很难统一。
响应式从 mobile-first 开始
Tailwind 的响应式是 mobile-first:没有前缀的 class 用于小屏,sm:、md:、lg: 用于更宽屏幕。初学者常见错误是先写桌面端 grid-cols-4,最后再补移动端,结果按钮、图片和标题高度全部错位。
请用 Tailwind CSS 改善商品列表。
要求:
- 375px 下 1 列,640px 以上 2 列,1024px 以上 3 列
- 图片保持正方形
- 卡片高度一致
- CTA 按钮固定在卡片底部
- 不改变 Product 的 props 类型
- 修改后检查移动端和桌面端截图
type Product = {
id: string;
name: string;
price: number;
imageUrl: string;
badge?: string;
};
export function ProductGrid({ products }: { products: Product[] }) {
return (
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</section>
);
}
function ProductCard({ product }: { product: Product }) {
return (
<article className="flex h-full flex-col overflow-hidden rounded-card border border-slate-200 bg-surface shadow-card dark:border-slate-800 dark:bg-slate-950">
<div className="relative aspect-square overflow-hidden bg-slate-50">
<img
src={product.imageUrl}
alt={product.name}
className="h-full w-full object-cover transition duration-200 hover:scale-105"
/>
</div>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 text-base font-semibold text-ink dark:text-white">
{product.name}
</h3>
<p className="mt-2 text-sm text-muted dark:text-slate-400">
¥{product.price.toLocaleString("zh-CN")}
</p>
<button className="mt-auto rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
查看详情
</button>
</div>
</article>
);
}
这里的关键是 aspect-square 固定图片比例,flex h-full flex-col 让卡片高度稳定,mt-auto 把按钮推到底部。
避免 class soup:重复组件再抽取
class soup 指 className 太长,开发者已经分不清哪些 class 是基础样式、哪些是状态、哪些是临时修补。Tailwind 不要求你把所有样式都抽进 CSS;相反,只有按钮、卡片、表单字段这种重复出现的 UI 才应该变成小组件。
import type { ButtonHTMLAttributes, ReactNode } from "react";
type ButtonVariant = "primary" | "secondary" | "danger";
const buttonVariants: Record<ButtonVariant, string> = {
primary: "bg-brand-600 text-white hover:bg-brand-700 focus:ring-brand-600",
secondary:
"border border-slate-300 bg-white text-slate-900 hover:bg-slate-50 focus:ring-slate-400 dark:border-slate-700 dark:bg-slate-900 dark:text-white",
danger: "bg-danger text-white hover:bg-red-700 focus:ring-danger",
};
function cn(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
loading?: boolean;
children: ReactNode;
};
export function Button({
variant = "primary",
loading = false,
disabled,
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
"inline-flex min-h-10 items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-slate-950",
buttonVariants[variant],
className,
)}
disabled={disabled || loading}
{...props}
>
{loading ? "处理中..." : children}
</button>
);
}
让 Claude Code 抽取组件时,要限制 variant 数量,并要求 class 以完整字符串存在。bg-${color}-600 这种写法看起来灵活,但 Tailwind 扫描时可能找不到它。
深色模式、表单和 CTA 要一起检查
深色模式不是最后补几个 dark:。背景、正文、边框、阴影、输入框、错误提示、focus ring 都要检查。表单和 CTA 是收益路径的一部分,更不能只看成功状态。
"use client";
import type { FormEvent } from "react";
type LeadFormProps = {
onSubmit: (values: { email: string; message: string }) => void;
error?: string;
};
export function LeadForm({ onSubmit, error }: LeadFormProps) {
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
onSubmit({
email: String(formData.get("email") ?? ""),
message: String(formData.get("message") ?? ""),
});
}
return (
<form
onSubmit={handleSubmit}
className="space-y-4 rounded-card border border-slate-200 bg-white p-5 shadow-card dark:border-slate-800 dark:bg-slate-950"
>
<label className="block text-sm font-medium text-slate-900 dark:text-white">
邮箱
<input
name="email"
type="email"
required
aria-describedby={error ? "lead-form-error" : undefined}
className="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-brand-600 focus:ring-2 focus:ring-brand-600/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
placeholder="you@example.com"
/>
</label>
{error ? (
<p id="lead-form-error" className="text-sm font-medium text-danger">
{error}
</p>
) : null}
<button className="w-full rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
咨询
</button>
</form>
);
}
如果要进一步检查可访问性,可以把Claude Code 无障碍指南也作为内部参考。
safelist 与 content scanning
Tailwind 会扫描源文件,只生成它能检测到的 class。动态拼接 class 是生产事故的常见原因。推荐写成静态 map。
type Status = "success" | "warning" | "danger";
const statusClasses: Record<Status, string> = {
success: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
warning: "bg-amber-50 text-amber-800 ring-amber-600/20",
danger: "bg-red-50 text-red-700 ring-red-600/20",
};
export function StatusBadge({ status, label }: { status: Status; label: string }) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${statusClasses[status]}`}
>
{label}
</span>
);
}
如果 class 来自外部 UI 包或 CMS,可以少量使用 @source 或 @source inline(),但不要把它当成万能修复。
@import "tailwindcss";
@source "../node_modules/@acme/ui-kit";
@source inline("bg-emerald-50");
@source inline("text-emerald-700");
@source inline("bg-amber-50");
@source inline("text-amber-800");
用截图固定视觉结果
类型检查和 build 成功并不代表 UI 正确。Tailwind 修改后,至少检查 375px、768px、1440px,并同时看浅色/深色模式。
import { expect, test } from "@playwright/test";
const viewports = [
{ name: "mobile", size: { width: 375, height: 812 } },
{ name: "tablet", size: { width: 768, height: 1024 } },
{ name: "desktop", size: { width: 1440, height: 960 } },
];
for (const viewport of viewports) {
test(`pricing page visual check - ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport.size);
await page.goto("/pricing");
await expect(page.getByRole("main")).toHaveScreenshot(
`pricing-${viewport.name}.png`,
{ maxDiffPixelRatio: 0.01 },
);
});
}
让 Claude Code 在最后输出:改了哪些文件、为什么改、哪些地方可能影响 CTA、哪些宽度已经检查、没有 Playwright 时如何手动检查。
适合的用例
| 用例 | 给 Claude Code 的任务 | Tailwind 重点 |
|---|---|---|
| 落地页 | 同时检查 hero、CTA、价格表、评价区 | 间距、标题层级、CTA 可见性 |
| SaaS 后台 | 梳理表格、筛选器、侧边栏、空状态 | 密度、横向滚动、sticky 区域 |
| 咨询表单 | 覆盖输入、错误、成功、loading、disabled | focus ring、标签、移动端触控 |
| 内容站 | 同时看正文、代码块、广告、CTA | 阅读宽度、代码滚动、内部链接 |
常见坑
- 使用
bg-${color}-600这类动态 class。 - 只看桌面端,不看 375px。
- 深色模式只改背景,忘记文字、边框和 focus。
- 用
@apply把所有东西藏进 CSS,反而看不出局部意图。 - 同一个项目里增加多套相似蓝色、灰色、阴影和圆角。
- 只检查表单成功状态,不看错误和 disabled。
- 不截图,导致 CTA、广告位或代码块重叠。
收益导线与总结
Tailwind 改造最终应该服务于业务目标。内容站要看文章末尾 CTA,模板商品要看价格卡,团队服务要看咨询表单。ClaudeCodeLab 提供免费 Claude Code cheatsheet、产品与模板、以及培训和咨询。
实际测试后,最有效的不是一次性大改,而是“先盘点、再改一个区域、最后做 375px 截图”。Masa 在 ClaudeCodeLab 的页面上这样做后,CTA 间距、表单 focus、深色模式对比度问题更早暴露,后续不再需要反复拆解过长的 className。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。