---
url: /blog-vue/a-first-look-at-nuxt-ui-v4.md
description: Nuxt UI 釋出 v4 了，免費開放許多進階元件，馬上來研究看看 ლ（´∀`ლ）
---

![a-first-look-at-nuxt-ui-v4](/a-first-look-at-nuxt-ui-v4.webp){.cover}

# 初探 Nuxt UI v4

Nuxt UI v4 熱騰騰釋出囉！%੭ ˙ᗜ˙ )੭%

期待很久了，終於等到這一天，馬上來研究看看 %ლ（´∀\`ლ）%

## 研究目標

雖然 Quasar 目前滿足大部分情境，但是 Quasar 更新放緩，作者心力分散至 Quasar CLI、SSR 開發，許多 UI 相關 issue 排不進排程，隨著需求與限制增加，可能會遇到瓶頸

而我目前僅用到 UI，至於為甚麼只用到 UI，以下是我的考量：

* Electron、Capacitor 整合：

  Quasar 雖然可以整合 Electron、Capacitor，但是截至目前的開發經驗，依照需求直接使用 Electron、Capacitor 會更好

  因為 Quasar CLI 二次封裝後，不但更難除錯（很難知道是 CLI 有問題、還是框架有問題），且封裝後許多設定參數脫離原框架的文件，很難找到參考資料

* SSR：

  我比較信任更成熟的 Nuxt，畢竟 SSR、SSG 是一個很複雜的問題，Nuxt 團隊有更多資源與人力可以解決問題

Nuxt UI 除了有官方背景外，還深度整合 Nuxt，潛力無窮，順便解決以下問題：

* Quasar utils class 衝突問題（因為統一使用 Tailwind CSS 了）
* Design Token 分散問題（雖然這個已經靠 UnoCSS 解決）
* SSR 整合問題（Quasar + Nuxt 有時候會遇到一些水合小毛病）
* Router 整合問題（Quasar + Nuxt 時，Router 跳轉按鈕不會使用 NuxtLink 元件）

## 背景資料

基於 [Reka UI](https://reka-ui.com/)、[Tailwind CSS](https://tailwindcss.com/) 與 [Tailwind Variants](https://www.tailwind-variants.org/) 開發，內部還整合了 VueUse、多個 `unplugin` 套件與資源，滿滿自家人的概念 %(◐‿◑)%

還有以下特點：

* MIT 授權
* a11y 完善
* VueUse 深度整合
* Nuxt SSR 整合
* Vue i18n 整合
* [元件超多](https://ui.nuxt.com/docs/components)

## 重點筆記

### 元件特點

以下筆記主要相對於 Quasar UI 出發。

### ui prop

[Nuxt UI：Customize components](https://ui.nuxt.com/docs/getting-started/theme/components#ui-prop)

```vue
<template>
  <u-button
    trailing-icon="i-lucide-chevron-right"
    size="md"
    color="neutral"
    variant="outline"
    :ui="{
      trailingIcon: 'rotate-90 size-3',
    }"
  >
    Button
  </u-button>
</template>
```

可以注入 class 至元件指定層級 DOM 中，這個功能與 `PrimeVue` 的 [`passthrough`](https://primevue.org/passthrough/) 類似

雖然實務開發上不是甚麼大問題，不過我自己有一個小疑問：基於 [BEM 規範](https://nedhsu.github.io/posts/CSS-Day25/)的 UI 套件若要魔改樣式，因為命名結構明確，可以很容易指向目標，但這類基於 Tailwind CSS 的 UI 套件若 `ui` 參數沒有開到，要指向特定的 DOM 是不是就需要相對複雜的 CSS 選擇器？

::: tip v4.2.0 新增 `data-slot` 屬性
v4.2.0 開始，對應 ui prop 的 DOM 會加入 `data-slot` 屬性，CSS 選擇器可以更容易指向目標 DOM 了 %(/≧▽≦)/%
:::

#### Form

[Nuxt UI：Form](https://ui.nuxt.com/docs/components/form)

欄位驗證可以直接用 Zod（支援 [Standard Schema](https://github.com/standard-schema/standard-schema) 的驗證器都可以）

配合 [ts-rest](https://codlin.me/blog-ocean-world/unleash-the-magic-of-type-script-with-ts-rest.html) 更簡單惹！%(ゝ ∀・)b%

#### Icon

內部使用 [unplugin-icons](https://github.com/unplugin/unplugin-icons)，[超多 icon](https://icones.js.org/collection/all) 可以用%！੭ ˙ᗜ˙ )੭%

會自動對 icon 進行按需引入（On-demand），不會載一大包字型檔案

![icon](/a-first-look-at-nuxt-ui-v4/icon.webp)

SSR、SSG 友善，不會有 [FOUC](https://inbound.technology/fouc-flash-of-unstyled-content-%E7%B6%B2%E9%A0%81%E6%A8%A3%E5%BC%8F%E7%9F%AD%E6%9A%AB%E5%A4%B1%E6%95%88%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F%E5%8E%9F%E5%9B%A0%E7%82%BA%E4%BD%95%EF%BC%9F/) 問題

#### Dashboard

[Nuxt UI：Dashboard](https://ui.nuxt.com/docs/components/dashboard-group)

一系列 Dashboard 元件，可以輕鬆拼出[專業後台](https://dashboard-vue-template.nuxt.dev/)

### Page

[Nuxt UI：Page](https://ui.nuxt.com/docs/components/page)

一系列 Page 元件，幾乎內建 [Landing 頁面](https://landing-template.nuxt.dev/)會用到的基礎元件了

綜合 Dashboard 元件，Nuxt UI 用來開發前後台都很適合

### Image

[Nuxt UI：ColorModeImage](https://ui.nuxt.com/docs/components/color-mode-image)

沒有額外封裝 img 元件，不過有提供一個 **`ColorModeImage`**，自動依照明暗模式切換圖片

若有安裝 `@nuxt/image` 會在內部自動依賴 `NuxtImg` 元件

#### Table

[Nuxt UI：Table](https://ui.nuxt.com/docs/components/table)

基於 [TanStack Table](https://tanstack.com/table/latest) 開發功能強大

cell 內容[同時支援 slot 與 h function](https://ui.nuxt.com/docs/components/table#with-slots) 更彈性

[`h` function](https://vuejs.org/guide/extras/render-function.html) 可以用來建立 vDOM，與 JSX 概念類似，適合用來渲染動態內容。

先前有一篇文章有提到其應用：[Vue h function 讓 Quasar Dialog 更容易重複使用](https://codlin.me/blog-vue/vue-h-function-makes-quasar-dialog-easier-to-reuse.html)
:::

如果內容只是簡單的調整樣式、排版，或簡單元件（例如：`Badge`、`Tag`） 等等，可以直接使用 `h` function：

```vue
<script setup lang="ts">
import type { TableColumn } from "@nuxt/ui";
import { h, resolveComponent } from "vue";

const UBadge = resolveComponent("UBadge");

const data = [
  // ...
];

const columns: TableColumn[] = [
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => {
      const color = {
        paid: "success" as const,
        failed: "error" as const,
        refunded: "neutral" as const,
      }[row.getValue("status") as string];

      return h(UBadge, { class: "capitalize", variant: "subtle", color }, () =>
        row.getValue("status")
      );
    },
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "amount",
    header: () => h("div", { class: "text-right" }, "Amount"),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue("amount"));

      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "EUR",
      }).format(amount);

      return h("div", { class: "text-right font-medium" }, formatted);
    },
  },
];
</script>

<template>
  <u-table :data="data" :columns="columns" class="flex-1" />
</template>
```

內容更內聚，不需要額外建立 slot 元件

但如果是較複雜的內容，還是使用 slot 更好，除了支援度更好外，template 編譯性能也比較好：

```vue
<script setup lang="ts">
import type { TableColumn } from "@nuxt/ui";

const data = [
  // ...
];

const columns: TableColumn[] = [
  {
    accessorKey: "id",
    header: "ID",
  },
  {
    accessorKey: "name",
    header: "Name",
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "role",
    header: "Role",
  },
  {
    id: "action",
  },
];
</script>

<template>
  <u-table :data="data" :columns="columns" class="flex-1">
    <template #name-cell="{ row }">
      <div class="flex items-center gap-3">
        <u-avatar
          :src="`https://i.pravatar.cc/120?img=${row.original.id}`"
          size="lg"
          :alt="`${row.original.name} avatar`"
        />
        <div>
          <p class="font-medium text-highlighted">
            {{ row.original.name }}
          </p>
          <p>
            {{ row.original.position }}
          </p>
        </div>
      </div>
    </template>
  </u-table>
</template>
```

#### Modal

Quasar 是 Dialog，在 Nuxt UI 中叫做 Modal

[Nuxt UI：Modal](https://ui.nuxt.com/docs/components/modal)

開啟方式不同於 Quasar，可以不用傳遞額外變數控制開啟（有需要的話還是可以綁定變數）

在 default slot 放入按鈕，即可以開啟，內容則寫在 content slot 中

```vue
<template>
  <u-modal>
    <u-button label="Open" color="neutral" variant="subtle" />

    <template #content>
      <placeholder class="h-48 m-4" />
    </template>
  </u-modal>
</template>
```

### Design System

承襲 Tailwind CSS v4 設計，設定變成基於 CSS 變數

[Nuxt UI：Design System](https://ui.nuxt.com/docs/getting-started/theme/design-system)

### Figma Kit

所有的顏色、樣式幾乎都已用 token 定義，與 CSS 名稱一致，可以直接複製貼上，元件也有定義相關變體，可以直接切換

這個資源很讚，這樣 UI 或 PM 要出設計稿或草稿時，都可以直接使用、不用重畫，更不容易出現認知落差。(・∀・)９

[Nuxt UI：Figma Kit](https://ui.nuxt.com/figma)

### Template

可以從模板借鑒~~抄襲~~內容過來用 (≖‿ゝ ≖)✧

[Nuxt UI：Templates](https://ui.nuxt.com/templates)

### AI 整合

#### MCP Server

可以與 AI 工具整合，甚至 [ChatGPT 也可以用](https://ui.nuxt.com/docs/getting-started/ai/mcp#chatgpt)

[Nuxt UI：MCP Server](https://ui.nuxt.com/docs/getting-started/ai/mcp)

安裝完成後使用指令（輸入 /）會出現以下提示：

![mcp](/a-first-look-at-nuxt-ui-v4/mcp.webp)

* **`find_component_for_usecase`**: Find the best component for your specific use

  給它功能情境，會幫你從 Nuxt UI 元件庫中挑最適合的元件（或元件組合），並說明為什麼、需要搭配哪些 props/slots/events、還會附上簡短使用片段。

* **`implement_component_with_props`**: Generate complete component implementation with proper
  當你已經決定「要用哪個 Nuxt UI 元件」與「要怎麼配置 props/事件/slots」，這個 prompt 會直接產出**完整可用的 SFC 實作**（含 `<script setup>` 型別、狀態、事件、slot 範例、a11y 小細節）。

* **`setup_project_with_template`**: Get guided setup instructions for project

  引導式教學，教你怎麼設定環境

#### LLMs.txt

可以在詢問時提及 [LLMs.txt URL](https://ui.nuxt.com/docs/getting-started/ai/llms-txt#other-ai-tools)，簡單來說就是一個適合 AI 閱讀文件，可以讓 AI 回應更精確

以下是 GPT-5 的問答結果：

**Table 提問：**

![llm-table](/a-first-look-at-nuxt-ui-v4/llm-table.webp)

**Modal 提問：**

![llm-modal](/a-first-look-at-nuxt-ui-v4/llm-modal.webp)

配合 MCP 與 LLMs.txt 可以有效降低 Nuxt UI 入門門檻 %✧⁑｡٩(ˊᗜˋ\*)و✧⁕｡%

## 問題討論

### 沒有 Directives、Utils

Quasar 內建有許多方便的 [directives](https://quasar.dev/vue-directives/touch-pan) 與 [utils](https://quasar.dev/quasar-utils/scrolling-utils)，不過 VueUse 有提供許多 directives 可以彌補這個缺點。

### 顯示引入元件

底層是 `unplugin-auto-import` 與 `unplugin-vue-components`，印象中可以這樣導入：

![auto-import](/a-first-look-at-nuxt-ui-v4/auto-import.webp)

不知道為甚麼不行，直接從 `@nuxt/ui` 引入也不行

不過還好可以根據目錄引入元件

```ts
import UModal from "@nuxt/ui/components/Modal.vue";
```

### ~~ui prop 與 DOM 結構~~

::: tip v4.2.0 新增 `data-slot` 屬性
v4.2.0 開始，對應 ui prop 的 DOM 會加入 `data-slot` 屬性
:::

ui prop 可以直接將 class 放到對應的 DOM 結構中，不過文件沒有明確說明 ui prop 具體對應哪一個 DOM？

`PrimeVue` 會在 DOM 之 attr 加入 `data-pc-section` 屬性，方便尋找。

雖然大部分元件的結構很單純，不難猜出位置，不過有些元件的 ui prop 有點多，例如 [CommandPalette](https://ui.nuxt.com/docs/components/command-palette#props)：

```
{
  root?: ClassNameValue;
  input?: ClassNameValue;
  close?: ClassNameValue;
  back?: ClassNameValue;
  content?: ClassNameValue;
  footer?: ClassNameValue;
  viewport?: ClassNameValue;
  group?: ClassNameValue;
  empty?: ClassNameValue;
  label?: ClassNameValue;
  item?: ClassNameValue;
  itemLeadingIcon?: ClassNameValue;
  itemLeadingAvatar?: ClassNameValue;
  itemLeadingAvatarSize?: ClassNameValue;
  itemLeadingChip?: ClassNameValue;
  itemLeadingChipSize?: ClassNameValue;
  itemTrailing?: ClassNameValue;
  itemTrailingIcon?: ClassNameValue;
  itemTrailingHighlightedIcon?: ClassNameValue;
  itemTrailingKbds?: ClassNameValue;
  itemTrailingKbdsSize?: ClassNameValue;
  itemLabel?: ClassNameValue;
  itemLabelBase?: ClassNameValue;
  itemLabelPrefix?: ClassNameValue;
  itemLabelSuffix?: ClassNameValue;
}
```

目前看來好像只能直接試試看了。%(´・ω・\`)%

### ~~Table 尚未支援 Virtual Scroll~~~

::: tip v4.1.0 後支援
[使用 `virtualize`](https://github.com/nuxt/ui/releases/tag/v4.1.0#:~:text=Component%20Virtualization) 即可啟用虛擬滾動 ੭ ˙ᗜ˙ )੭
:::

底層的 TanStack Table 可以使用 [`@tanstack/vue-virtual`](https://tanstack.com/virtual/latest/docs/introduction) 支援虛擬滾動，不過 Nuxt UI 的 Table 尚未支援，只能等待更新了

[Table 原始碼](https://github.com/nuxt/ui/blob/v4/src/runtime/components/Table.vue)沒有複雜依賴，真的需要也是可以考慮自行加上 `@tanstack/vue-virtual` 封裝

### useToast 位置不能單一設定

::: tip
感謝狐狸大大的補充 (\*´∀\`)~♥
:::

Toast 位置只能[全域設定](https://ui.nuxt.com/docs/components/toast#change-global-position)，不能像 Quasar 那樣每個 Toast 可以獨立設定位置

[官方說](https://github.com/nuxt/ui/issues/808) Headless UI 和 Radix Vue 都不支援單一設定，目前沒有計畫支援 %(´・ω・\`)%

不過實際上影響不大，沒事不會一直換位置 XD

### useOverlay

[文件](https://ui.nuxt.com/docs/composables/use-overlay)預期用來開啟 Modal 元件，這樣的缺點是需建立一個被開啟元件的 Modal 變體，使用上沒那麼方便，所以同[先前 Quasar Dialog 的封裝](https://codlin.me/blog-vue/vue-h-function-makes-quasar-dialog-easier-to-reuse)，我也做了一個 `openUsingModal` function。

````typescript
import type { Component } from "vue";
import type {
  ComponentProps,
  ComponentSlots,
} from "vue-component-type-helpers";
import UModal from "@nuxt/ui/components/Modal.vue";
import { h } from "vue";

type ModelProps = ComponentProps<typeof UModal>;

/** 將 Vue SFC 元件包装為 UModal，可以更簡單配合 useOverlay 使用
 *
 * @param component Vue SFC 元件
 * @param props SFC 內所有參數，包含 class、style、event 等等
 * @param modalProps UModal 原本參數
 * @param slots SFC 插槽
 */
function wrapWithModal<Comp extends Component>(
  component: Comp,
  props?: ComponentProps<Comp>,
  modalProps?: ModelProps,
  slots?: ComponentSlots<Comp>
) {
  return h(
    UModal,
    {
      class: "w-auto!",
      ...modalProps,
    },
    {
      body: () => h(component, props ?? {}, slots ?? {}),
    }
  );
}

/** 使用 useOverlay + UModal 開啟元件
 *
 * @param component Vue SFC 元件
 * @param props SFC 內所有參數，包含 class、style、event 等等
 * @param modalProps UModal 原本參數
 * @param slots SFC 插槽
 *
 * @example
 * ```typescript
 * const dialog = openUsingModal(BrandEditStatusForm, {
 *   data,
 *   onSuccess() {
 *     dialog.hide();
 *     handleEditSuccess();
 *   },
 * });
 * ```
 */
export function openUsingModal<Comp extends Component>(
  component: Comp,
  props?: ComponentProps<Comp>,
  modalProps?: ModelProps,
  slots?: ComponentSlots<Comp>
) {
  const overlay = useOverlay();

  const modal = overlay.create(
    wrapWithModal(
      component,
      props,
      {
        close: false,
        ui: { body: "p-0!" },
        ...modalProps,
      },
      slots
    ),
    { props }
  );
  modal.open();

  return modal;
}
````

### 沒有簡易所見及所得編輯器

需要額外引入，目前候選為：

* Tiptap：MIT
* TinyMCE：GPLv2+

TinyMCE 功能強大，不過 GPLv2+ 授權不太適合商業專案

### 自定義 Form 欄位

Quasar 可以使用 [`useFormChild`](https://quasar.dev/vue-composables/use-form-child#syntax) 或 [`Field` 元件](https://quasar.dev/vue-components/field)包裝，自定義 Form 內的驗證欄位

研究了一下，其實有提供 `useFormField`，只是不知道為甚麼文件網頁沒有出現，但[文件原始碼有此 md 檔](https://github.com/nuxt/ui/blob/v4/docs/content/docs/3.composables/use-form-field.md)。

### ~~沒有 Time Picker~~

::: tip v4.2.0 後新增 `UInputDate`、`UInputTime`
可喜可賀！%੭ ˙ᗜ˙ )੭%

[Nuxt UI：UInputDate](https://ui.nuxt.com/docs/components/input-date)

[Nuxt UI：UInputTime](https://ui.nuxt.com/docs/components/input-time)
:::

[官方說](https://github.com/nuxt/ui/issues/957)沒有計畫。%...(›´ω\`‹ )%

欸不是，都有 Calendar 了，做個 Time Picker 不過份吧！%ლ（´ 口\`ლ）%

只能自己拼一個或是用 input 將 type 設為 time 版本

不過原生的 time input 在 window 的 chrome 上有點醜就是了 %(。-\`ω´-)%

```vue
<UInput v-model="data" type="time" />
```

![input-time](/a-first-look-at-nuxt-ui-v4/input-time.webp)

## 總結 🐟

* Nuxt UI v4 元件多、功能強大且免費，潛力無窮
* Nuxt 深度整合，SSR、SSG 友善
* VueUse、Vue i18n、unplugin 等等 Vue 生態系深度整合，可以省去不少設定時間
* AI 整合（MCP、LLMs.txt），降低入門門檻

以上就是這次初探，其他細節或坑就要等到實務開發時再慢慢研究了 %◝( •ω• )◟%

Nuxt UI v4 整體感覺很不錯，不過有一個小小遺憾，就是[不能使用 UnoCSS](https://github.com/nuxt/ui/issues/196) %( ˘･з･)%

雖然有基於 UnoCSS 的 [Una UI](https://github.com/una-ui/una-ui)，不過考量生態系問題，還是先用 Nuxt UI 比較實際
