
一年後再看 Nuxt UI
大家好,我是鱈魚。(。・∀・)ノ゙
去年九月寫過一篇 初探 Nuxt UI v4,文末列了一串「目前還沒有的東西」。
不到一年,Nuxt UI 已經從 v4.0 一路衝到 v4.11,元件數從 113 個長到 133 個 ੭ ˙ᗜ˙ )੭
感謝 Vercel 乾爹 XD
來先驗收以前列的坑,再挑幾個這一年來有沒有甚麼有趣的酷功能!੭ ˙ᗜ˙ )੭
先看版本節奏
從去年底到現在的釋出時間表,大概一個月一個 minor:
| 版本 | 時間 | 招牌功能 |
|---|---|---|
| v4.1.0 | 2025/10 | Empty、五個元件支援虛擬化、componentDetection |
| v4.2.0 | 2025/11 | InputDate/InputTime、data-slot、Tailwind prefix |
| v4.3.0 | 2025/12 | Editor 全家桶、ScrollArea |
| v4.4.0 | 2026/01 | Calendar 週次、CommandPalette 調校 |
| v4.5.0 | 2026/02 | Theme 元件、四款新中性色、Toast 去重 |
| v4.6.0 | 2026/03 | Sidebar、AI Chat 元件三兄弟 |
| v4.7.0 | 2026/04 | Listbox、Link 自動 i18n |
| v4.8.0 | 2026/05 | Theme 元件可設 prop 預設值、ContentSearch 非同步搜尋 |
| v4.9.0 | 2026/06 | Calendar 月/年模式、useTour、theme.unstyled |
| v4.10.0 | 2026/07 | InputRating、icon 打包進 build、unmountOnHide |
| v4.11.0 | 2026/08 | Splitter、ProgressGroup、Vue 端 CSS 掃描剔除 |
節奏穩定到有點可怕,官方團隊真的很拚 (*´∀`)~♥
舊坑驗收時間
先給大家看結論表,細節在後面。
| 當初的抱怨 | 現況 |
|---|---|
| Table 沒有虛擬滾動 | ✅ v4.1 補上,而且一次給了五個元件 |
| ui prop 不知道對到哪個 DOM | ✅ v4.2 data-slot,後面又加了 Theme 元件 |
| 沒有 Time Picker | ✅ v4.2 UInputDate、UInputTime |
| 沒有所見即所得編輯器 | ✅ v4.3 直接送一整套 |
useFormField 文件找不到 | 🔶 Form 文件裡有提到,但獨立頁面反而消失了 |
元件無法從 @nuxt/ui 引入 | ❌ 還是只能走目錄 |
| 沒有 directives、utils | ❌ 還是沒有 |
| Toast 不能單獨設定位置 | ❌ 官方已標記 not planned |
| 不能用 UnoCSS | ❌ issue 早就 not planned 了 |
九項裡面補了四項半,以一年來說算相當有誠意 (・∀・)9
✅ 所見即所得編輯器
這是我最意外的一項。當初的結論是「要自己引 Tiptap 或 TinyMCE」,結果 v4.3.0 官方直接端出六個元件:
Editor:本體,支援 JSON、HTML、Markdown 三種內容格式EditorToolbar:工具列,可自訂功能群組EditorSuggestionMenu:打/叫出的指令選單EditorMentionMenu:打@提及某人EditorEmojiMenu:打:插入 emojiEditorDragHandle:拖曳把手,可重新排序區塊
底層就是 Tiptap,MIT 授權,沒有授權疑慮惹!੭ ˙ᗜ˙ )੭
最基本的用法只有一行:
<script setup lang="ts">
const value = ref('# Hello World')
</script>
<template>
<UEditor v-model="value" content-type="markdown" class="w-full min-h-21" />
</template>v-model 綁下去、content-type 選格式,就這樣 (๑•̀ㅂ•́)و✧
官方還附了 Editor 模板,裡面有用 PartyKit 做的即時協作,還有接 AI SDK 的自動補字。
想加自訂功能的話,Tiptap 的 extension 系統整套開放,editor 實例也拿得到,天花板很高。
TIP
Tiptap 相關套件掛在 peerDependencies,記得自己裝。
✅ ui prop 與 DOM 結構
當初的痛點是 ui 參數一堆 key,不知道對應到哪層 DOM。v4.2 加了 data-slot 已經緩解一半,這一年又多了三個新解法。
第一招,Theme 元件(v4.5.0)。
不用一個一個元件改,包起來就好:
<template>
<UTheme
:ui="{
button: { base: 'rounded-full' },
input: { base: 'rounded-full' },
}"
>
<UButton label="Button" color="neutral" variant="outline" />
<UInput placeholder="Search..." />
</UTheme>
</template>它不渲染任何 DOM,底層走 Vue 的 provide / inject,所以不管巢狀多深都吃得到。可以層層嵌套,內層蓋外層,元件上直接寫的 ui 優先權最高。
第二招,Theme 元件也能設 prop 預設值(v4.8.0)。
<template>
<UTheme
:props="{
tooltip: { delayDuration: 0, arrow: true },
button: { color: 'neutral', variant: 'subtle', size: 'lg' },
input: { size: 'lg' },
}"
>
<UTooltip text="Tooltip">
<UButton label="Save" />
</UTooltip>
</UTheme>
</template>某個區塊要整批換成大尺寸?包一層就好,再也不用複製貼上 size="lg" 三十次 (´,,•ω•,,)♡
第三招,slot class 可以寫成 function(v4.9.0)。
以前傳字串是「合併」到預設 class 上,遇到要覆蓋的情境就得跟 tailwind-merge 角力。現在傳 function 就變成「取代」:
<template>
<!-- 完全取代 base 的預設 class -->
<UButton :ui="{ base: () => 'text-3xl font-bold' }" label="Button" />
<!-- 想留一部分也可以,參數就是解析後的預設值 -->
<UButton :ui="{ base: (defaults) => `${defaults} text-3xl` }" label="Button" />
</template>:ui、app.config.ui、<UTheme :ui> 三個地方都通用。
加碼,theme.unstyled(v4.9.0)。
如果想連預設樣式都不要,直接在 build 階段剝光:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
theme: {
unstyled: true,
},
},
})只留結構與 a11y 邏輯,樣式全部自己來,順便減少 HTML 與 bundle 肥肉。
路人:「你看起來也很需要 unstyled。( ・ิω・ิ)」
鱈魚:「說好的尊重呢?!Σ(ˊДˋ;)」
WARNING
連結構性 class 也會一起剝掉(定位、transition、flex/grid 都算)。像 Modal、Drawer、Calendar 這種版面吃重的元件,得自己重新排版。
🔶 自定義 Form 欄位
當初的狀況是「useFormField 存在,但文件網頁沒有」。
現在反過來了,docs/content/docs/3.composables/use-form-field.md 這個檔案直接從 repo 消失,改成在 Form 文件的自訂驗證段落用一句 tip 帶過:
You can use the
useFormFieldcomposable to implement this inside your own components.
功能還在、src/runtime/composables/useFormField.ts 也還在,就是沒有獨立說明頁。想知道回傳哪些東西,還是得翻原始碼 (´・ω・`)
❌ 元件顯式引入
還是老樣子。看一下 @nuxt/ui 的 exports 欄位就知道原因:
{
".": {
"types": "./dist/module.d.mts",
"import": "./dist/module.mjs"
},
"./components/*": "./dist/runtime/components/*",
"./composables/*": {
"types": "./dist/runtime/composables/*.d.ts",
"import": "./dist/runtime/composables/*.js"
}
}根路徑導出的是 Nuxt module 本體,所以 import { UModal } from '@nuxt/ui' 永遠拿不到元件。乖乖走目錄:
import UModal from '@nuxt/ui/components/Modal.vue'好消息是 ./composables 與 ./utils 這兩個路徑開出來了,寫共用邏輯時方便一點。
❌ Directives、Utils
翻了一下 src/runtime,資料夾只有 components、composables、locale、plugins、types、utils、vue,沒有 directives。
utils 裡面是 ai.ts、form.ts、link.ts、tv.ts 這類內部工具,跟 Quasar 那種通用工具庫是兩回事。
這塊還是繼續靠 VueUse 補,實務上影響不大。
❌ Toast 單獨設定位置
官方 issue 在 2024 年底就標記 not planned 了,現在文件還是同一句話:
Change the
toaster.positionprop on the App component to change the position of the toasts.
不過 v4.5.0 加了我覺得更實用的 Toast 去重。同樣 id 的 toast 再叫一次,現有那張會跳一下脈動動畫,畫面上永遠只有一張:
const toast = useToast()
// 連按十次也只會有一張,只是會抖十下 XD
toast.add({ id: 'save-success', title: '儲存成功' })手殘連點的使用者終於不會噴出一整排通知了 (≧∀≦)
❌ UnoCSS
issue #196 2023 年就 not planned 結案,六十幾個讚也救不回來。
現在 tailwindcss 掛在 dependencies 裡(v4.3.3),tailwind-variants、tailwind-merge 也是核心依賴,這條路基本上封死了。想用 UnoCSS 就只能看 Una UI,不過生態系差距擺在那,我還是繼續用 Nuxt UI ( ˘・з・)
酷酷的新東西
除了上面補坑的部分,還有一票純新增的功能。
Splitter(v4.11.0)
熱騰騰,八月才出的可拖曳分割版面:
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const items: SplitterItem[] = [
{ slot: 'sidebar', minSize: 15, maxSize: 40, defaultSize: 25 },
{ slot: 'main', defaultSize: 75 },
]
</script>
<template>
<USplitter id="layout" :items="items" class="h-96">
<template #sidebar>
Sidebar
</template>
<template #main>
Main
</template>
</USplitter>
</template>支援 min/max 尺寸、可收合、可巢狀、可直向。SSR 記得給 id 讓水合時對得上,另外 auto-save-id 會把版面存進 localStorage,使用者調好的比例下次還在 (ノ>ω<)ノ
寫 IDE 風格的工具或後台時超好用。
Sidebar(v4.6.0)
以前要自己拼 Dashboard 元件才做得出來的側邊欄,現在有專門元件了。桌機固定、手機自動變成 Modal、Slideover 或 Drawer:
<template>
<USidebar v-model:open="open" collapsible="icon">
<template #header>
<logo />
</template>
<UNavigationMenu :items="items" />
<template #footer>
<user-menu />
</template>
</USidebar>
</template>三種外觀(sidebar、floating、inset)配三種收合模式(offcanvas、icon、none),排列組合夠用了。
WARNING
v4.6.0 改用 Nuxt 的 moduleDependencies API,最低要求變成 Nuxt 4.1.0,升級前先確認一下。
useTour(v4.9.0)
新手導覽終於不用自己刻,用法很簡單,整趟導覽共用同一個 Popover,換步驟時把錨點重新指過去就好:
<script setup lang="ts">
const card = useTemplateRef('card')
const tour = useTour([
{ target: '#cta', title: 'Get started' },
{ target: () => card.value, title: 'Profile', side: 'right' },
{ target: null, title: 'All set' },
])
</script>
<template>
<UButton @click="tour.start()">
Start tour
</UButton>
<UPopover
:open="tour.open.value"
:reference="tour.reference.value"
:dismissible="false"
>
<template #content>
<UButton :disabled="!tour.hasPrev.value" @click="tour.prev()">
Back
</UButton>
<UButton @click="tour.next()">
{{ tour.hasNext.value ? "Next" : "Finish" }}
</UButton>
</template>
</UPopover>
</template>target 收字串選擇器、function 或 null(null 就是置中不指任何東西)。它只管步驟狀態與錨點解析,內容跟按鈕全部交給你,彈性剛剛好。
會自動把目標捲進畫面,切換時 popover 也會平滑移動 (ゝ∀・)b
虛擬滾動與 Listbox
v4.1.0 補虛擬滾動時,官方一口氣給了五個元件,CommandPalette、InputMenu、SelectMenu、Table、Tree 通通追加了 virtualize 參數:
<template>
<UTable :data="data" :columns="columns" virtualize />
<USelectMenu v-model="value" :items="items" virtualize />
</template>ScrollArea(v4.3.0)則是有內建虛擬滾動的捲動容器,底層一樣是 TanStack Virtual,任何自己刻的長清單都能套。
Listbox(v4.7.0)是常駐顯示的可選清單,內建搜尋與虛擬化。跟 SelectMenu 的差別在於它不會彈出浮層:
<script setup lang="ts">
const items = ref([
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
])
const value = ref()
</script>
<template>
<UListbox v-model="value" :items="items" />
</template>雙欄式的挑選介面(左邊清單、右邊詳情)就靠它了。
unmountOnHide(v4.10.0)
這招很實用。Modal 與 Slideover 關閉時預設會卸載內容,表單填一半關掉就沒了。設成 false 就會留著:
<template>
<UModal :unmount-on-hide="false">
<UButton label="Open" />
<template #content>
<!-- 關掉也還在 -->
</template>
</UModal>
</template>表單狀態、捲動位置、初始化很貴的子元件,都能撐過開開關關 (´▽`ʃ♡ƪ)
Icon 打包進 build(v4.10.0)
以前 icon 是執行時去 Iconify API 撈,離線就 GG。現在會直接嵌進 build 產物,SSR 當下就渲染得出來,離線也能用。
Nuxt 端靠 @nuxt/icon 的 client bundle 自動處理。純 Vue + Vite 專案則是 @nuxt/ui/vite 多了 icon.clientBundle 選項:
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
ui({
icon: {
clientBundle: {
// 手動列
icons: ['lucide:heart', 'simple-icons:github'],
// 或整包掃
scan: true,
},
},
}),
],
})記得把用到的 collection 裝起來(@iconify-json/{collection_name}),不然沒東西可以打包。
CSS 掃描剔除終於下放到 Vue(v4.11.0)
這功能其實 v4.1.0 就有了,叫做 experimental.componentDetection。打開之後會掃過你的原始碼,只替真正用到的元件(連同它們的相依元件)產出 CSS:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
experimental: {
componentDetection: true,
},
},
})一百多個元件的樣式全塞進去實在有點浪費,掃過一輪只留用到的,CSS 直接瘦一大圈。
v4.2.0 又補了兩個洞,lazy component 掃得到了,Nuxt Layers 也會一起掃。
不過這一年來它都只有 Nuxt module 吃得到,純 Vue + Vite 專案只能眼巴巴看著。(╥ω╥`)
直到 v4.11.0,@nuxt/ui/vite 終於也支援了:
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
ui({
experimental: {
componentDetection: true,
},
}),
],
})用 <component :is> 動態渲染的元件掃不到,改傳陣列手動列出來即可。
Nuxt 與純 Vue 兩邊的功能落差又補平一項 (´,,•ω•,,)♡
純 Vue 的待遇一路補上來
把這一年的更新排在一起看,會發現純 Vue 專案的待遇正在追上 Nuxt。
- v4.3.0:
unplugin加了router與scanPackages選項,也支援 prose 元件 - v4.6.0:
Icon開放 Vue 端的全域設定,官方還架了獨立的 Vue REPL playground - v4.9.0:
vite外掛加了root選項,可以自訂.nuxt-ui目錄位置 - v4.10.0:icon 打包進 Vite build
- v4.11.0:
componentDetection下放
文件也多了一整頁純 Vue 的 SSR 指南。Nuxt 那邊開箱即用,純 Vue 得自己用 @unhead/vue/server 把顏色變數注進 <head>,不然 SSR 會少一截樣式:
import { createHead, renderSSRHead } from '@unhead/vue/server'
const head = createHead()
const payload = await renderSSRHead(head)
app.head.push(payload.headTags)Laravel Inertia 與 AdonisJS 的完整範例文件裡也都有,這兩個生態系要接 Nuxt UI 變得容易很多。
小插曲
當初在 Vite + Vue 專案照文件設定完,發現 theme 相關的 prop 型別提示缺一塊。
研究後發現是 tsconfig 少了 #build/ui/* 這條 path,補上就好了:
{
"compilerOptions": {
"paths": {
"#build/ui/*": ["./node_modules/.nuxt-ui/ui/*"]
}
}
}PR 在 v4.4.0 釋出。
雖然只是改了一點點文件,不過能夠協助貢獻感覺很不錯 (≧∀≦)
Tailwind class 前綴(v4.2.0)
這個功能對舊專案來說很關鍵。
Nuxt UI 會產出一大堆 Tailwind utility class,跟你自己的樣式或其他 UI 函式庫撞名的機率不低。v4.2.0 開放了 Tailwind CSS 的 prefix 選項:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
ui: {
theme: {
prefix: 'tw',
},
},
})CSS 那邊也要對應改一下:
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";之後所有 utility 都變成 tw:flex、tw:p-4 這種樣子,跟既有樣式井水不犯河水。
要把 Nuxt UI 塞進既有專案漸進式導入時,這招很實用。(´,,•ω•,,)♡
更加 AI 友善:Agent Skills
去年那篇寫了 MCP Server 與 LLMs.txt,這一年官方又多開了一條路,Agent Skills。
跟 MCP 提供即時工具呼叫不同,Skills 是把結構化的知識檔直接塞進 AI 的上下文,整段對話都參考得到。官方的 usage skill 涵蓋安裝、主題、全部元件、composables、表單驗證、版面組合與官方模板。
安裝靠 skills CLI,支援三十幾種 agent:
# 裝進目前專案
npx skills add nuxt/ui
# 指定 agent
npx skills add nuxt/ui --agent claude-code
# 全域安裝,所有專案共用
npx skills add nuxt/ui --global裝完在對話框打 /nuxt-ui 就能叫出來。
Claude Code 也可以直接用自己的指令裝:
claude skill add https://github.com/nuxt/ui/tree/v4/skills/nuxt-uiSkill 檔案本身就放在 repo 的 skills/nuxt-ui/,任何吃自訂上下文的工具都能引用。
順帶一提,ProsePrompt 元件在 v4.10.0 多了 claude 這個 action,文件網頁上的範例 prompt 可以一鍵丟進 Claude (・∀・)9
動畫節奏統一(v4.11.0)
所有動畫與位移轉場(含離場)統一走 --ease-out。想調整整套函式庫的手感,覆寫一個 token 就好:
@theme {
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}prefers-reduced-motion 的覆蓋範圍也擴大了,浮層改成原地淡入,Tabs 指示器之類的位移轉場會停掉。無障礙有顧到,讚讚 (ゝ∀・)b
其他小東西
- Empty(v4.1.0):空狀態元件,
icon、title、description、actions、loading一次到位,不用每個專案自己刻一份 - InputRating(v4.10.0):星等評分,支援半星、hover 預覽、自訂 icon
- ProgressGroup(v4.11.0):多段式進度條配圖例,硬碟容量分析那種畫面
- Calendar
typeprop(v4.9.0):一個參數在日/月/年選擇器之間切換,標題也變成可點的按鈕,不用狂按上一頁下一頁 - AI Chat 三兄弟(v4.6.0):
ChatReasoning、ChatTool、ChatShimmer,串 AI SDK 的訊息 parts 就能做出有思考過程與工具呼叫的聊天介面 - 四款新中性色(v4.5.0):
taupe、mauve、mist、olive,來自 Tailwind CSS v4.2 - Link 自動 i18n(v4.7.0):裝了
@nuxtjs/i18n之後,所有吃to參數的元件自動走$localePath - 統一 focus 樣式(v4.9.0):所有元件共用同一套
focus-visible光暈,顏色跟著元件的color走 - Form 程式化送出也走 HTML5 驗證(v4.5.0):呼叫
form.submit()時會先跑原生驗證,送出按鈕放在 Modal footer 那種情境特別有用 - Table row pinning(v4.6.0):可以把指定列釘在上下兩端,v4.7.0 又補上虛擬化模式下的 sticky header/footer
useToast的max(v4.1.0):全域設定同時最多顯示幾張通知Popover的enableTouch(v4.10.0):觸控裝置上也能觸發,久違的 #2346 終於結案- Prose 標題錨點與複製按鈕可設定(v4.10.0):拿 Nuxt UI 寫文件站的話會用到
Checkbox/Switch的trueValue/falseValue(v4.6.0):接後端那種'Y'/'N'欄位不用再自己轉- icon 可以用
false隱藏(v4.9.0):不用再想辦法塞空字串 - 新增 composable 文件:
useScrollShadow(捲動陰影)、extractShortcuts(從選單項目自動抽出快捷鍵)、defineLocale與extendLocale
升級要注意的地方
跨這麼多版本,難免有 breaking change。
不過不是問題,當然是鞭打 AI 叫他給我升到好。 ◝( •ω• )◟
| 版本 | 異動 |
|---|---|
| v4.1.0 | CommandPalette 的 trailing-icon 改給 input 用,子項目的 icon 改用 children-icon |
| v4.1.0 | Table 的 select 事件參數順序調整 |
| v4.2.0 | InputMenu、InputNumber、InputTags、Select、SelectMenu 的 exposed ref 統一回傳 HTML 元素,不再給元件實例 |
| v4.6.0 | 改用 Nuxt 的 moduleDependencies API,最低需求變成 Nuxt 4.1.0 |
| v4.8.0 | InputMenu 的 autocomplete 改名 mode,值為 'combobox'(預設)或 'autocomplete' |
v4.8.0 那個改名非改不可。原本的 autocomplete 跟 HTML 原生屬性撞名,瀏覽器自動填入(email、one-time-code 那些)會失效。改名之後原生屬性就能正常穿透到內層 input 了:
- <UInputMenu autocomplete :items="items" />
+ <UInputMenu mode="autocomplete" :items="items" />官方在 v4.1.0 的 release note 還特地道歉了一下,說一百多個元件偶爾就是得修正一致性。我是覺得這種頻率完全可以接受啦 (´∀`)
總結 🐟
- 一年內從 v4.0 到 v4.11,元件 113 個變 133 個,一個月一個 minor,節奏穩定得誇張
- 當初列的九個坑補掉四個半,Editor 全家桶影響最直接,商業專案的授權疑慮直接解決;剩下三個(顯式引入、directives、UnoCSS)是架構取向問題,短期內不用期待
- Theme 元件加上 slot class function、
theme.unstyled、theme.prefix,客製化路線從「猜 DOM」變成「有工具可用」 - 純 Vue 專案的待遇一路補上來,SSR 文件、icon 打包、
componentDetection陸續下放,Nuxt 與 Vue 的落差越來越小 - AI 整合從 MCP、LLMs.txt 延伸到 Agent Skills,一行
npx skills add nuxt/ui就到位
一年前的結論是「潛力無窮」,一年後看起來,的確不斷成長。( •̀ ω •́ )✧
如果去年因為缺編輯器或缺虛擬滾動而放棄的大大們,現在可以回鍋看看了 ლ(´∀`ლ)
以上如有錯誤,還請多多指教。
感謝您讀到這裡,如果您覺得有收穫,歡迎分享出去。