很多团队在 Vue 3 + Vite 项目里想用 shadcn-vue,但又不想引入完整的 Tailwind CSS 体系。原因也很现实:项目里已经用了 UnoCSS,或者更喜欢 UnoCSS 的按需生成、配置灵活和原子类体验。
这篇文章记录一套可落地的方案:在 Vue 3 + Vite 中,使用 UnoCSS + @unocss/preset-wind4 + unocss-preset-shadcn 来替代传统的 Tailwind CSS + shadcn-vue 组合,同时保留 shadcn-vue 的组件组织方式、变量主题和设计令牌。
先说结论:这套方案是可行的,但它不是官方默认路径。shadcn-vue 官方文档默认还是围绕 Tailwind CSS 展开,所以我们需要做一层“兼容桥接”。
为什么要这样做
传统的 shadcn-vue 接入方式,通常依赖:
shadcn-vue init
tailwindcss
postcss
然后再通过 Tailwind 的配置、扫描、CSS variables 和组件生成机制把整套东西串起来。
如果项目本身已经使用 UnoCSS,那么再引入一整套 Tailwind,通常会带来几个问题:
- 样式体系重复,维护成本上升
- 工程配置变重
- 团队要同时理解 UnoCSS 和 Tailwind 两套配置
- shadcn-vue 实际上只需要“类名语义 + CSS 变量 + 若干工具函数”,并不一定强依赖 Tailwind 运行时
unocss-preset-shadcn 的价值就在这里:它把 shadcn 的类名语义映射到了 UnoCSS 上,让我们可以继续使用 shadcn-vue 的组件风格,但底层由 UnoCSS 驱动。
这套方案的核心思路
我们并不是“把 shadcn-vue 改造成 UnoCSS 项目”,而是做三件事:
- 用 UnoCSS 提供原子类生成能力
- 用
@unocss/preset-wind4提供接近 Tailwind v4 的语义 - 用
unocss-preset-shadcn提供 shadcn 所需的 token、颜色、半径、语义类支持
同时保留这些 shadcn-vue 生态要求:
components.json@/lib/utilscn()工具函数- CSS variables
@路径别名reka-ui作为底层依赖
先说几个必须注意的坑
这是整篇文章最重要的部分。
第一,shadcn-vue 官方默认流程是 Tailwind 路线,不是 UnoCSS 路线。
所以如果你直接照官方文档一步步执行,再中途改成 UnoCSS,通常会出现配置半 Tailwind 半 UnoCSS 的混乱状态。
第二,components.json 仍然要保留。
即使你不用 Tailwind,这个文件也仍然是 shadcn-vue CLI 和组件结构识别的重要入口。
第三,通常仍然需要一个“占位用”的 tailwind.config.js。
不是因为项目真的要跑 Tailwind,而是因为不少工具链和 CLI 会检查这个文件是否存在。
第四,unocss-preset-shadcn 和 preset-wind4 在 TypeScript 类型层面可能有冲突。
这是一个很容易误判为“导入报错”的问题。很多时候不是 import { presetWind4 } ... 有问题,而是 presetShadcn() 的 theme 类型和 presetWind4() 的 theme 类型不完全一致。最简单的处理方式是给它们做 Preset 类型断言。
第五,底层组件库要统一。
现在的 shadcn-vue 应优先使用 reka-ui,不要再混用旧的 radix-vue 风格依赖。
依赖安装
如果你的项目已经是 Vue 3 + Vite,只需要补齐这几个依赖。
运行时依赖:
pnpm add class-variance-authority clsx tailwind-merge lucide-vue-next reka-ui unocss-preset-animations unocss-preset-shadcn
开发依赖:
pnpm add -D unocss @unocss/preset-wind4 @unocss/reset @unocss/transformer-directives @unocss/transformer-variant-group shadcn-vue
这里有几个包的作用要理解:
@unocss/preset-wind4:提供接近 Tailwind v4 的类名语义unocss-preset-shadcn:给 shadcn 的语义类和 token 做 UnoCSS 映射class-variance-authority:shadcn 组件常用的 variants 方案clsx + tailwind-merge:配合cn()使用reka-ui:shadcn-vue 组件底层依赖shadcn-vue:CLI 和 registry 能力
配置 Vite
在 vite.config.ts 中启用 UnoCSS,并配置 @ 别名:
import path from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
export default defineConfig({
plugins: [vue(), UnoCSS()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
这一步的意义很简单:
UnoCSS()负责样式生成@别名是 shadcn-vue 组件组织的基础
配置 TypeScript 别名
在 tsconfig.json 里配置路径:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
在 tsconfig.app.json 里同样保留:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
如果你使用的是 TypeScript 6,不建议再写 baseUrl,否则会看到废弃提示。
配置 UnoCSS
这是整个方案的核心。
uno.config.ts:
import presetWind from '@unocss/preset-wind4'
import transformerDirectives from '@unocss/transformer-directives'
import transformerVariantGroup from '@unocss/transformer-variant-group'
import type { Preset } from 'unocss'
import { defineConfig } from 'unocss'
import presetAnimations from 'unocss-preset-animations'
import { presetShadcn } from 'unocss-preset-shadcn'
export default defineConfig({
presets: [
presetWind() as Preset,
presetAnimations() as Preset,
presetShadcn(
{
color: 'neutral',
},
{
componentLibrary: 'reka',
},
) as Preset,
],
transformers: [transformerDirectives(), transformerVariantGroup()],
content: {
pipeline: {
include: [
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
'(components|src)/**/*.{js,ts}',
],
},
},
})
这里重点解释一下:
presetWind()提供基础原子类能力presetShadcn()提供 shadcn 主题类和变量支持componentLibrary: 'reka'很关键include中额外加上js/ts,否则一些 shadcn 组件里的类名可能不会被扫描到as Preset是为了绕开当前生态里的一些类型兼容问题
创建 components.json
虽然我们不用 Tailwind,但仍然建议保留 components.json:
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/assets/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}
这一步的本质不是“启用 Tailwind”,而是“满足 shadcn-vue 的组件配置约定”。
最重要的几个字段:
style: "new-york":决定组件风格cssVariables: true:必须开ui、utils、lib等别名必须和项目真实路径一致
创建占位的 tailwind.config.js
这个文件可以非常简单:
export default {}
它存在的意义不是让 Tailwind 工作,而是让某些 CLI 或检测逻辑不要报“缺失 Tailwind 配置”。
入口样式和 CSS Variables
在 src/main.ts 中引入:
import { createApp } from 'vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
import './assets/index.css'
import App from './App.vue'
createApp(App).mount('#app')
这里的顺序建议保持:
- reset
- uno.css
- 自定义变量样式
然后在 src/assets/index.css 中维护 shadcn 所需的变量:
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
你也可以继续补 .dark 下的变量,实现暗黑模式。
这一步非常关键,因为 shadcn 的视觉体系本质上依赖的是 CSS variables,而不是某个特定的 CSS 引擎。
准备 cn() 工具函数
在 src/lib/utils.ts 中:
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
虽然我们用的是 UnoCSS,但 shadcn-vue 组件模板普遍会依赖这个 cn()。
组件怎么来
这里有两条路。
第一条路,理想路径:继续用 shadcn-vue add button 这类命令。
第二条路,现实路径:如果 CLI 在你的环境里卡住,直接通过 shadcn-vue 的 registry 拉取组件模板。
在不少项目里,CLI 并不会像 Tailwind 官方路径那样顺滑,因为它默认还是围绕 Tailwind 工作流设计的。所以你要有一个预期:
“UnoCSS 方案能用,但 CLI 体验未必和官方 Tailwind 流程完全一致。”
不过即使 CLI 偶尔不顺,组件本身仍然是标准 Vue 文件,你完全可以把 registry 的组件代码放进:
src/components/ui/button/Button.vue
src/components/ui/button/index.ts
然后正常使用。
为什么很多人会以为是 import 报错
一个很常见的误区是:
import presetWind from '@unocss/preset-wind4'
或者:
import { presetWind4 } from '@unocss/preset-wind4'
编辑器标红,于是大家以为是导入方式错了。
但很多时候,真正的问题不是导入,而是 presets 数组里的 theme 类型冲突。
presetShadcn() 当前的类型声明更偏向 preset-mini 那一套 theme,而 presetWind4() 返回的是另一套 theme 类型。TypeScript 会在数组收敛时把错误标在 presetWind() 附近,给人一种“导入错了”的错觉。
所以这类问题要这样理解:
- 语法没错
- 包也装了
- 真正的问题是 preset 之间的类型兼容
这套方案适合什么场景
如果你的项目满足下面几个条件,这套方案会很合适:
- 已经在用 UnoCSS
- 不想额外引入完整 Tailwind 工程链
- 想保留 shadcn-vue 的组件风格和组织方式
- 能接受“这不是官方默认路径”带来的少量兼容处理
如果你们团队更看重“严格官方支持、CLI 一路丝滑、出问题更容易搜答案”,那还是 Tailwind 官方路线更省心。
实际收益
落地之后,你会得到这些好处:
- 保留 UnoCSS 的轻量和按需生成
- 保留 shadcn-vue 的组件组织方式
- 保留 CSS variables 主题能力
- 不需要在项目里维护完整 Tailwind 配置链
- 后续新增组件时,工程结构依然清晰
尤其对于已经有 UnoCSS 基础的团队,这套方案能明显减少样式体系分裂的问题。
一份最小工作清单
如果你只想看执行顺序,可以照这个 checklist 走:
- 安装 UnoCSS、wind4、shadcn 相关依赖
- 在
vite.config.ts中启用UnoCSS() - 配置
@别名 - 配置
tsconfig路径映射 - 新建
components.json - 新建占位
tailwind.config.js - 配置
uno.config.ts - 在
main.ts中引入@unocss/reset/tailwind.css、uno.css、自定义变量 CSS - 新建
src/lib/utils.ts - 添加第一个
Button组件验证通路
结语
Vue 3 + Vite + UnoCSS + shadcn-vue 不是一条“默认路线”,但它是一条完全值得走的路线。
它的关键不是“完全摆脱 shadcn-vue 的约定”,而是理解 shadcn-vue 真正依赖的是什么:
- 组件结构
- 变量系统
- 工具函数
- 语义类名
一旦这些东西都接好了,底层到底是 Tailwind 还是 UnoCSS,就不再是阻碍,而只是样式生成器的选择。
如果你的项目已经拥抱 UnoCSS,那么用 unocss-preset-shadcn 取代传统 Tailwind 方案,是一条很自然、也很有工程价值的演进路径。
评论区