Nuxt 的 UI 库怎么选:从 Nuxt UI 迁到 shadcn-vue
这个站最早用的是 Nuxt UI,今年 7 月迁到了 shadcn-vue。这篇先把 Vue/Nuxt 生态里常见的几个 UI 库摆出来比一比,再讲我为什么迁、迁的时候踩了什么坑、迁完之后得到了什么。
候选
| 库 | 怎么装 | 样式 | 适合 |
|---|---|---|---|
| Nuxt UI | npm 包 + Nuxt 模块 | Tailwind,靠主题配置覆盖 | Nuxt 项目,后台、SaaS、文档站 |
| shadcn-vue | CLI 把源码复制进项目 | Tailwind,直接改源码 | 有自己设计的站 |
| PrimeVue | npm 包 | 自带主题,也可以关掉,自己用 Tailwind 画 | 表格多、表单多的企业后台 |
| Vuetify | npm 包 | Material Design | 接受 Material 风格的应用 |
| Element Plus / Naive UI | npm 包 | 自带主题 | 国内常见的管理后台 |
如果你的项目是 Nuxt,又打算用 Tailwind,真正要选的其实只有前两个。PrimeVue 值得多提一句:它出了一个叫 Volt 的方案,把组件源码复制进你的项目,用 Tailwind 重新画样式。走的是和 shadcn 一样的路,可见「源码归你」这个思路已经不只是 shadcn 一家在做了。
同一个底座,两种分发方式
Nuxt UI 和 shadcn-vue 的底层几乎一样:交互和无障碍都靠 Reka UI(Vue 版的 Radix),样式都用 Tailwind v4。所以两边弹窗的焦点管理、下拉菜单的键盘操作,本质上是同一套实现。
区别在分发方式:
- Nuxt UI 是一个库。 组件在
node_modules里,你通过它提供的主题系统去改样式。 - shadcn-vue 不是库。
npx shadcn-vue add button会把 Button 的源码复制到你的components/ui/button/里,之后这就是你自己的代码。
选哪个,其实就是在选:样式是通过配置去覆盖别人的默认值,还是直接改自己的源码。
Nuxt UI:开箱即用
先说它的好。2025 年 9 月发布的 Nuxt UI v4 把原来收费的 Pro 版合并进来,全部 MIT 开源,有 110 多个组件,还附带模板和 Figma 设计稿。装上之后它会自动注册 @nuxt/icon、@nuxt/fonts 和 @nuxtjs/color-mode:图标、字体、深浅色模式都不用自己配。做后台或者 SaaS,这种全家桶很省事。
改样式有四层,优先级从低到高:
app.config.ts里的全局主题:按组件的 slot(组件内部的各个元素)和 variant 改<UTheme>组件:只影响它包住的部分- 组件实例上的
uiprop:改这一个实例的某个 slot class:改根元素
底层用 tailwind-merge 合并 class,冲突的工具类后写的赢。
我遇到的问题:一直在关掉它的默认样式
问题出在我想要的样子和它的默认样子差得比较远。翻迁移前的代码,全站有 17 处 ui prop,大部分是这样的:
<UModal v-model:open="isOpen"
:ui="{ content: 'bg-transparent shadow-none ring-0 border-none overflow-visible' }">
这一行的意思是:把弹窗自带的背景、阴影、描边全部关掉,然后我在里面自己再画一张卡片。同样一行在 5 个弹窗里各写了一遍。输入框也是:
<UInput :ui="{ base: 'px-3.5 py-2.5 bg-slate-100/50 dark:bg-slate-900/30 border border-slate-200/60 dark:border-slate-800/80 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/55 transition-all text-sm shadow-none' }" />
一模一样的一长串,复制了 6 遍。
公平地说,这些本来可以集中写进 app.config.ts,是我偷懒了。但即使写进全局配置,做的事情也一样:先知道弹窗里那一层叫 content、输入框那一层叫 base,再写一串 class 去压过它的默认值。slot 叫什么、默认值是什么,都得去翻文档或者主题源码。
另一个原因更主观:Nuxt UI 的默认审美偏后台 SaaS。拿来做管理界面很合适,但我想要的个人站是另一种感觉。
shadcn-vue:组件是自己的代码
迁到 shadcn-vue 之后,同样的需求变成了直接改源码。比如输入框的聚焦样式,shadcn 默认是边框变色,外面再加一圈 3px 的半透明 ring,看起来像两道框。我想要一条干净的实线,就直接改 components/ui/input/Input.vue:
// 聚焦:边框变 brand,再贴一圈 1px 同色的 ring,看起来是一条 2px 的实线
'focus-visible:border-brand focus-visible:ring-1 focus-visible:ring-brand',
改一个文件,全站的输入框都变了。不用知道 slot 名,也不用和默认值比优先级,因为默认值就写在我眼前。
这个例子也说明,shadcn 的默认样式不是不用改,而是改的地方不一样:在 Nuxt UI 里,你是在覆盖别人的默认值;在 shadcn 里,默认值本身就是你的代码。
它的代价也很明确:
- 没有全家桶。 深浅色模式、图标、字体都要自己装、自己配。
- 一个一个组件装。 用到哪个,
add哪个。 - 上游更新不会自动跟过来。 源码归你,维护也归你。shadcn 修了某个组件的 bug,你得自己去对比、合并。
- 复杂组件少。 没有 Nuxt UI 那样现成的仪表盘、聊天界面,需要的话得自己拼。
还有一个附带的好处:组件源码就在项目里,用 AI 写代码时,它能直接读到、直接改,不用去猜 node_modules 里的实现。
迁移
实际迁移的量不大:用到的 Nuxt UI 组件一共 8 种,调用点 54 处。
| Nuxt UI | shadcn-vue |
|---|---|
| UButton | Button |
| UInput / UTextarea | Input / Textarea |
| UModal | Dialog |
| USlideover | Sheet |
| UDropdownMenu | DropdownMenu |
| USelectMenu | Select |
| useToast | vue-sonner |
有两处值得一说。
Toast。 shadcn-vue 的 toast 用的是 vue-sonner,API 和 Nuxt UI 的 useToast().add() 不一样。我写了一个同名的 useToast,保留 .add() 的签名,底层换成 vue-sonner。这样所有调用点一行都不用改。
深浅色模式悄悄坏了。 删掉 @nuxt/ui 之后,build 通过,页面也正常,但主题切换按钮点了没反应。原因是 @nuxtjs/color-mode 以前是 Nuxt UI 自动注册的,我从来没在 modules 里写过它。Nuxt UI 一删,它也跟着没了,而 useColorMode() 不会报错。解决方法是装回来,并且设置 classSuffix: '':color-mode 默认给 <html> 加的是 dark-mode 这个 class,而 Tailwind 的 dark: 只认 dark。
所以从 Nuxt UI 迁出去之前,先列一下它替你注册了哪些模块,迁移时一个一个补回来。
迁完之后
我把迁移前后的两个提交各构建了一次,比较首次打开页面时必须下载的入口 JS 和 CSS(gzip 之后):
| 迁移前 | 迁移后 | 变化 | |
|---|---|---|---|
| 入口 JS | 202 KB | 192 KB | −5% |
| 入口 CSS | 45 KB | 33 KB | −27% |
JS 只少了 5%,比我预想的少。Nuxt UI 的组件是按需引入的,没用到的组件本来就不会打进包里,所以 JS 这边没什么可省的。省下来的主要是 CSS:Tailwind 会扫描 Nuxt UI 的主题文件,把每个组件所有颜色、所有变体的 class 都生成出来,哪怕我只用了其中一种;shadcn 的源码里只有我实际写下的 class。
还要说明一点:这次迁移的同一个提交里,Nuxt 也从 4.3 升到了 4.5,所以这组数字不完全是换 UI 库这一件事带来的。可以当作量级参考,别当成精确的基准测试。
现在 components/ui 里只有 8 个组件:button、dialog、dropdown-menu、input、select、sheet、sonner、textarea。站上用到什么就有什么,没有别的。
怎么选
- 做后台、SaaS、文档站,想要开箱即用,也能接受它的默认审美:Nuxt UI。它的全家桶和现成的复杂组件,能省下大量时间。
- 有自己的设计,组件用得不多,想把每个像素都掌握在自己手里:shadcn-vue。
- 表格、表单多的企业后台:PrimeVue。想要 Tailwind 加源码归自己,可以看它的 Volt。
- 就要 Material 风格:Vuetify。
对我这个站来说,迁移是值得的。一是好看:清爽、克制,更接近我想要的样子;二是轻:用到什么装什么,CSS 少了四分之一。多出来的配置工作,是一次性的。