1. @apply:提取组件 vs 不提取
@apply 允许在 CSS 中复用工具类,将重复的类名组合提取为自定义类。但它存在明显权衡——使用前务必思考清楚。
适合 @apply 的场景
- 跨多个 HTML 文件重复的组合
- 第三方 HTML(不能改 class)
- Markdown 生成的内容
不适合 @apply 的场景
- 在 React/Vue 中可以提取组件
- 只用了一两次的样式
- 团队约定用 utility-first 风格
/* ⚠️ 权衡:@apply 让 HTML 更干净,但失去了 utility-first 的灵活性 */
@layer components {
.btn {
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg font-semibold
transition-all duration-200 focus:outline-none focus-visible:ring-2;
}
.btn-primary {
@apply bg-cyan-500 text-white hover:bg-cyan-600 active:bg-cyan-700
focus-visible:ring-cyan-500 focus-visible:ring-offset-2;
}
.btn-outline {
@apply border-2 border-cyan-500 text-cyan-600 hover:bg-cyan-50
dark:hover:bg-cyan-950;
}
.input {
@apply w-full px-3 py-2 rounded-lg border border-gray-300
focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500
dark:bg-gray-800 dark:border-gray-600 dark:text-white
outline-none transition-shadow;
}
}
<!-- 使用:HTML 更简洁 -->
<button class="btn btn-primary">提交</button>
<button class="btn btn-outline">取消</button>
<input class="input" placeholder="请输入">
2. shadcn/ui:复制粘贴组件库
shadcn/ui 是目前最流行的 Tailwind 组件解决方案,它不是传统 npm 包,而是"复制粘贴"模式——你把组件源码直接复制到项目中,完全拥有所有权,可以随意修改。
Headless UI:只提供行为(可访问性、键盘导航、ARIA 属性),不提供样式的 UI 库。代表作有 Headless UI(Tailwind Labs 出品)和 Radix UI。shadcn/ui 就是在 Radix UI 基础上加了 Tailwind 样式。
# 安装 shadcn/ui(以 Next.js 项目为例)
npx shadcn@latest init
# 添加需要的组件(会复制源码到 src/components/ui/)
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
npx shadcn@latest add form input
3. 完整表单组件实现
<!-- 完整的联系表单 -->
<form class="space-y-6 bg-white dark:bg-gray-900 rounded-2xl p-8 max-w-lg">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">联系我们</h2>
<!-- 双列:姓名 + 邮箱 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
姓名 <span class="text-red-500">*</span>
</label>
<input
type="text"
class="
peer w-full px-3 py-2 rounded-lg text-sm
border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-800 text-gray-900 dark:text-white
placeholder:text-gray-400
focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent
invalid:border-red-400 invalid:focus:ring-red-400
transition-shadow
"
placeholder="张三"
required
>
<p class="hidden peer-invalid:block text-xs text-red-500 mt-1">请填写姓名</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">邮箱</label>
<input type="email" class="w-full px-3 py-2 rounded-lg text-sm border border-gray-300
focus:outline-none focus:ring-2 focus:ring-cyan-500
dark:bg-gray-800 dark:border-gray-600 dark:text-white"
placeholder="user@example.com">
</div>
</div>
<!-- Select 下拉 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">问题类型</label>
<select class="w-full px-3 py-2 rounded-lg text-sm border border-gray-300
focus:outline-none focus:ring-2 focus:ring-cyan-500
dark:bg-gray-800 dark:border-gray-600 dark:text-white bg-white">
<option>技术问题</option>
<option>账单问题</option>
<option>产品反馈</option>
</select>
</div>
<!-- Textarea -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">详细描述</label>
<textarea rows="4" class="w-full px-3 py-2 rounded-lg text-sm border border-gray-300
focus:outline-none focus:ring-2 focus:ring-cyan-500
dark:bg-gray-800 dark:border-gray-600 dark:text-white
resize-none"
placeholder="请详细描述您的问题..."></textarea>
</div>
<!-- 提交按钮 -->
<button type="submit"
class="w-full bg-cyan-500 hover:bg-cyan-600 active:bg-cyan-700
text-white font-semibold py-3 rounded-xl
transition-colors duration-200
focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500">
发送消息
</button>
</form>
4. Modal 弹窗组件
<!-- 弹窗遮罩 + 内容 -->
<div id="modal" class="fixed inset-0 z-50 flex items-center justify-center p-4
bg-black/50 backdrop-blur-sm
hidden data-[open]:flex">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-xl
w-full max-w-md max-h-[90vh] overflow-y-auto
animate-fade-up">
<!-- 弹窗头部 -->
<div class="flex items-center justify-between px-6 py-4
border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">确认操作</h3>
<button onclick="closeModal()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300
p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">✕</button>
</div>
<!-- 弹窗内容 -->
<div class="px-6 py-4">
<p class="text-gray-600 dark:text-gray-400">你确定要执行这个操作吗?</p>
</div>
<!-- 弹窗底部 -->
<div class="flex gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<button class="flex-1 px-4 py-2 rounded-lg border border-gray-300
text-gray-700 hover:bg-gray-50 font-medium">取消</button>
<button class="flex-1 px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600
text-white font-medium">确认删除</button>
</div>
</div>
</div>
本章小结:在 React/Vue 项目中,优先通过提取组件来复用样式,而非 @apply。shadcn/ui 是目前最符合 Tailwind 精神的组件库——"复制粘贴"模式让你完全掌控代码,不受黑盒限制。表单和弹窗是最常用的组件,掌握这两个模式可以覆盖 80% 的 UI 需求。