Skip to content

nuxt-ui-v4-revisited

一年後再看 Nuxt UI

大家好,我是鱈魚。(。・∀・)ノ゙

去年九月寫過一篇 初探 Nuxt UI v4,文末列了一串「目前還沒有的東西」。

不到一年,Nuxt UI 已經從 v4.0 一路衝到 v4.11,元件數從 113 個長到 133 個 ੭ ˙ᗜ˙ )੭

感謝 Vercel 乾爹 XD

來先驗收以前列的坑,再挑幾個這一年來有沒有甚麼有趣的酷功能!੭ ˙ᗜ˙ )੭

先看版本節奏

從去年底到現在的釋出時間表,大概一個月一個 minor:

版本時間招牌功能
v4.1.02025/10Empty、五個元件支援虛擬化、componentDetection
v4.2.02025/11InputDate/InputTime、data-slot、Tailwind prefix
v4.3.02025/12Editor 全家桶、ScrollArea
v4.4.02026/01Calendar 週次、CommandPalette 調校
v4.5.02026/02Theme 元件、四款新中性色、Toast 去重
v4.6.02026/03Sidebar、AI Chat 元件三兄弟
v4.7.02026/04Listbox、Link 自動 i18n
v4.8.02026/05Theme 元件可設 prop 預設值、ContentSearch 非同步搜尋
v4.9.02026/06Calendar 月/年模式、useTour、theme.unstyled
v4.10.02026/07InputRating、icon 打包進 build、unmountOnHide
v4.11.02026/08Splitter、ProgressGroup、Vue 端 CSS 掃描剔除

節奏穩定到有點可怕,官方團隊真的很拚 (*´∀`)~♥

舊坑驗收時間

先給大家看結論表,細節在後面。

當初的抱怨現況
Table 沒有虛擬滾動✅ v4.1 補上,而且一次給了五個元件
ui prop 不知道對到哪個 DOM✅ v4.2 data-slot,後面又加了 Theme 元件
沒有 Time Picker✅ v4.2 UInputDateUInputTime
沒有所見即所得編輯器✅ 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:打 : 插入 emoji
  • EditorDragHandle:拖曳把手,可重新排序區塊

底層就是 Tiptap,MIT 授權,沒有授權疑慮惹!੭ ˙ᗜ˙ )੭

最基本的用法只有一行:

vue
<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)。

不用一個一個元件改,包起來就好:

vue
<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)。

vue
<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 就變成「取代」:

vue
<template>
  <!-- 完全取代 base 的預設 class -->
  <UButton :ui="{ base: () => 'text-3xl font-bold' }" label="Button" />

  <!-- 想留一部分也可以,參數就是解析後的預設值 -->
  <UButton :ui="{ base: (defaults) => `${defaults} text-3xl` }" label="Button" />
</template>

:uiapp.config.ui<UTheme :ui> 三個地方都通用。

加碼,theme.unstyled(v4.9.0)。

如果想連預設樣式都不要,直接在 build 階段剝光:

ts
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  ui: {
    theme: {
      unstyled: true,
    },
  },
})

只留結構與 a11y 邏輯,樣式全部自己來,順便減少 HTML 與 bundle 肥肉。


路人:「你看起來也很需要 unstyled。( ・ิω・ิ)

鱈魚:「說好的尊重呢?!Σ(ˊДˋ;)


WARNING

連結構性 class 也會一起剝掉(定位、transition、flex/grid 都算)。像 ModalDrawerCalendar 這種版面吃重的元件,得自己重新排版。

🔶 自定義 Form 欄位

當初的狀況是「useFormField 存在,但文件網頁沒有」。

現在反過來了,docs/content/docs/3.composables/use-form-field.md 這個檔案直接從 repo 消失,改成在 Form 文件的自訂驗證段落用一句 tip 帶過:

You can use the useFormField composable to implement this inside your own components.

功能還在、src/runtime/composables/useFormField.ts 也還在,就是沒有獨立說明頁。想知道回傳哪些東西,還是得翻原始碼 (´・ω・`)

❌ 元件顯式引入

還是老樣子。看一下 @nuxt/uiexports 欄位就知道原因:

json
{
  ".": {
    "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' 永遠拿不到元件。乖乖走目錄:

ts
import UModal from '@nuxt/ui/components/Modal.vue'

好消息是 ./composables./utils 這兩個路徑開出來了,寫共用邏輯時方便一點。

❌ Directives、Utils

翻了一下 src/runtime,資料夾只有 componentscomposableslocalepluginstypesutilsvue,沒有 directives

utils 裡面是 ai.tsform.tslink.tstv.ts 這類內部工具,跟 Quasar 那種通用工具庫是兩回事。

這塊還是繼續靠 VueUse 補,實務上影響不大。

❌ Toast 單獨設定位置

官方 issue 在 2024 年底就標記 not planned 了,現在文件還是同一句話:

Change the toaster.position prop on the App component to change the position of the toasts.

不過 v4.5.0 加了我覺得更實用的 Toast 去重。同樣 id 的 toast 再叫一次,現有那張會跳一下脈動動畫,畫面上永遠只有一張:

ts
const toast = useToast()

// 連按十次也只會有一張,只是會抖十下 XD
toast.add({ id: 'save-success', title: '儲存成功' })

手殘連點的使用者終於不會噴出一整排通知了 (≧∀≦)

❌ UnoCSS

issue #196 2023 年就 not planned 結案,六十幾個讚也救不回來。

現在 tailwindcss 掛在 dependencies 裡(v4.3.3),tailwind-variantstailwind-merge 也是核心依賴,這條路基本上封死了。想用 UnoCSS 就只能看 Una UI,不過生態系差距擺在那,我還是繼續用 Nuxt UI ( ˘・з・)

酷酷的新東西

除了上面補坑的部分,還有一票純新增的功能。

Splitter(v4.11.0)

熱騰騰,八月才出的可拖曳分割版面:

vue
<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 風格的工具或後台時超好用。

以前要自己拼 Dashboard 元件才做得出來的側邊欄,現在有專門元件了。桌機固定、手機自動變成 Modal、Slideover 或 Drawer:

vue
<template>
  <USidebar v-model:open="open" collapsible="icon">
    <template #header>
      <logo />
    </template>

    <UNavigationMenu :items="items" />

    <template #footer>
      <user-menu />
    </template>
  </USidebar>
</template>

三種外觀(sidebarfloatinginset)配三種收合模式(offcanvasiconnone),排列組合夠用了。

WARNING

v4.6.0 改用 Nuxt 的 moduleDependencies API,最低要求變成 Nuxt 4.1.0,升級前先確認一下。

useTour(v4.9.0)

新手導覽終於不用自己刻,用法很簡單,整趟導覽共用同一個 Popover,換步驟時把錨點重新指過去就好:

vue
<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 或 nullnull 就是置中不指任何東西)。它只管步驟狀態與錨點解析,內容跟按鈕全部交給你,彈性剛剛好。

會自動把目標捲進畫面,切換時 popover 也會平滑移動 (ゝ∀・)b

虛擬滾動與 Listbox

v4.1.0 補虛擬滾動時,官方一口氣給了五個元件,CommandPaletteInputMenuSelectMenuTableTree 通通追加了 virtualize 參數:

vue
<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 的差別在於它不會彈出浮層:

vue
<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)

這招很實用。ModalSlideover 關閉時預設會卸載內容,表單填一半關掉就沒了。設成 false 就會留著:

vue
<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 選項:

ts
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:

ts
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 終於也支援了:

ts
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 加了 routerscanPackages 選項,也支援 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 會少一截樣式:

ts
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,補上就好了:

json
{
  "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 選項:

ts
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      prefix: 'tw',
    },
  },
})

CSS 那邊也要對應改一下:

css
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";

之後所有 utility 都變成 tw:flextw:p-4 這種樣子,跟既有樣式井水不犯河水。

要把 Nuxt UI 塞進既有專案漸進式導入時,這招很實用。(´,,•ω•,,)♡

更加 AI 友善:Agent Skills

去年那篇寫了 MCP Server 與 LLMs.txt,這一年官方又多開了一條路,Agent Skills

跟 MCP 提供即時工具呼叫不同,Skills 是把結構化的知識檔直接塞進 AI 的上下文,整段對話都參考得到。官方的 usage skill 涵蓋安裝、主題、全部元件、composables、表單驗證、版面組合與官方模板。

安裝靠 skills CLI,支援三十幾種 agent:

bash
# 裝進目前專案
npx skills add nuxt/ui

# 指定 agent
npx skills add nuxt/ui --agent claude-code

# 全域安裝,所有專案共用
npx skills add nuxt/ui --global

裝完在對話框打 /nuxt-ui 就能叫出來。

Claude Code 也可以直接用自己的指令裝:

bash
claude skill add https://github.com/nuxt/ui/tree/v4/skills/nuxt-ui

Skill 檔案本身就放在 repo 的 skills/nuxt-ui/,任何吃自訂上下文的工具都能引用。

順帶一提,ProsePrompt 元件在 v4.10.0 多了 claude 這個 action,文件網頁上的範例 prompt 可以一鍵丟進 Claude (・∀・)9

動畫節奏統一(v4.11.0)

所有動畫與位移轉場(含離場)統一走 --ease-out。想調整整套函式庫的手感,覆寫一個 token 就好:

css
@theme {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

prefers-reduced-motion 的覆蓋範圍也擴大了,浮層改成原地淡入,Tabs 指示器之類的位移轉場會停掉。無障礙有顧到,讚讚 (ゝ∀・)b

其他小東西

  • Empty(v4.1.0):空狀態元件,icontitledescriptionactionsloading 一次到位,不用每個專案自己刻一份
  • InputRating(v4.10.0):星等評分,支援半星、hover 預覽、自訂 icon
  • ProgressGroup(v4.11.0):多段式進度條配圖例,硬碟容量分析那種畫面
  • Calendar type prop(v4.9.0):一個參數在日/月/年選擇器之間切換,標題也變成可點的按鈕,不用狂按上一頁下一頁
  • AI Chat 三兄弟(v4.6.0):ChatReasoningChatToolChatShimmer,串 AI SDK 的訊息 parts 就能做出有思考過程與工具呼叫的聊天介面
  • 四款新中性色(v4.5.0):taupemauvemistolive,來自 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
  • useToastmax(v4.1.0):全域設定同時最多顯示幾張通知
  • PopoverenableTouch(v4.10.0):觸控裝置上也能觸發,久違的 #2346 終於結案
  • Prose 標題錨點與複製按鈕可設定(v4.10.0):拿 Nuxt UI 寫文件站的話會用到
  • CheckboxSwitchtrueValuefalseValue(v4.6.0):接後端那種 'Y' / 'N' 欄位不用再自己轉
  • icon 可以用 false 隱藏(v4.9.0):不用再想辦法塞空字串
  • 新增 composable 文件useScrollShadow(捲動陰影)、extractShortcuts(從選單項目自動抽出快捷鍵)、defineLocaleextendLocale

升級要注意的地方

跨這麼多版本,難免有 breaking change。

不過不是問題,當然是鞭打 AI 叫他給我升到好。 ◝( •ω• )◟

版本異動
v4.1.0CommandPalettetrailing-icon 改給 input 用,子項目的 icon 改用 children-icon
v4.1.0Tableselect 事件參數順序調整
v4.2.0InputMenuInputNumberInputTagsSelectSelectMenu 的 exposed ref 統一回傳 HTML 元素,不再給元件實例
v4.6.0改用 Nuxt 的 moduleDependencies API,最低需求變成 Nuxt 4.1.0
v4.8.0InputMenuautocomplete 改名 mode,值為 'combobox'(預設)或 'autocomplete'

v4.8.0 那個改名非改不可。原本的 autocomplete 跟 HTML 原生屬性撞名,瀏覽器自動填入(emailone-time-code 那些)會失效。改名之後原生屬性就能正常穿透到內層 input 了:

diff
- <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.unstyledtheme.prefix,客製化路線從「猜 DOM」變成「有工具可用」
  • 純 Vue 專案的待遇一路補上來,SSR 文件、icon 打包、componentDetection 陸續下放,Nuxt 與 Vue 的落差越來越小
  • AI 整合從 MCP、LLMs.txt 延伸到 Agent Skills,一行 npx skills add nuxt/ui 就到位

一年前的結論是「潛力無窮」,一年後看起來,的確不斷成長。( •̀ ω •́ )✧

如果去年因為缺編輯器或缺虛擬滾動而放棄的大大們,現在可以回鍋看看了 ლ(´∀`ლ)

以上如有錯誤,還請多多指教。

感謝您讀到這裡,如果您覺得有收穫,歡迎分享出去。