---
title: "Migration to v4"
description: "A comprehensive guide to migrate your application from Nuxt UI v3 to Nuxt UI v4."
canonical_url: "https://ui.nuxt.com/docs/getting-started/migration/v4"
last_updated: "2026-04-30"
---
# Migration to v4

> A comprehensive guide to migrate your application from Nuxt UI v3 to Nuxt UI v4.

Nuxt UI v4 marks a major milestone: **Nuxt UI and Nuxt UI Pro are now unified into a single, fully open-source and free library**. You now have access to 125+ production-ready components, all available in the `@nuxt/ui` package.

> [!NOTE]
> 
> Nuxt UI v4 requires **Nuxt 4** due to some dependencies. Make sure to upgrade to Nuxt 4 before migrating to Nuxt UI v4.

This guide provides step-by-step instructions to migrate your application from v3 to v4.

## Migrate your project

### From Nuxt UI Pro

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your `package.json`:

```bash [pnpm]
pnpm remove @nuxt/ui-pro
pnpm add @nuxt/ui tailwindcss
```

```bash [yarn]
yarn remove @nuxt/ui-pro
yarn add @nuxt/ui tailwindcss
```

```bash [npm]
npm uninstall @nuxt/ui-pro
npm install @nuxt/ui tailwindcss
```

```bash [bun]
bun remove @nuxt/ui-pro
bun add @nuxt/ui tailwindcss
```

**Nuxt:**

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your `nuxt.config.ts`:

```diff [nuxt.config.ts]
export default defineNuxtConfig({
  modules: [
-   '@nuxt/ui-pro',
+   '@nuxt/ui'
  ]
})
```

**Vue:**

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your `vite.config.ts`:

```diff [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
- import uiPro from '@nuxt/ui-pro/vite'
+ import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
-   uiPro({
+   ui({
      ui: {
        colors: {
          primary: 'green',
          neutral: 'slate'
        }
      }
    })
  ]
})
```

**Nuxt:**

1. Use the `ui` key instead of `uiPro` in your `app.config.ts`:

```diff [app/app.config.ts]
export default defineAppConfig({
  ui: {
    colors: {
      primary: 'green',
      neutral: 'slate'
    },
+   pageCard: {
+     slots: {
+       root: 'rounded-xl',
+     }
+   }
  },
- uiPro: {
-   pageCard: {
-     slots: {
-       root: 'rounded-xl',
-     }
-   }
- }
})
```

**Vue:**

1. Use the `ui` key instead of `uiPro` in your `vite.config.ts`:

```diff [vite.config.ts]
export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        colors: {
          primary: 'green',
          neutral: 'slate'
        },
+       pageCard: {
+         slots: {
+           root: 'rounded-xl',
+         }
+       }
      },
-     uiPro: {
-       pageCard: {
-         slots: {
-           root: 'rounded-xl',
-         }
-       }
-     }
    })
  ]
})
```

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your CSS:

**Nuxt:**

```diff [app/assets/css/main.css]
@import "tailwindcss";
- @import "@nuxt/ui-pro";
+ @import "@nuxt/ui";
```

> [!WARNING]
> 
> If you are upgrading to Nuxt 4 at the same time as Nuxt UI v4, make sure to update the `@source` directive to match the new directory structure.
> 
> ```diff [app/assets/css/main.css]
> @import "tailwindcss";
> @import "@nuxt/ui";
> 
> - @source "../../content/**/*";
> + @source "../../../content/**/*";
> ```

**Vue:**

```diff [src/assets/css/main.css]
@import "tailwindcss";
- @import "@nuxt/ui-pro";
+ @import "@nuxt/ui";
```

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your imports:

```diff
- import type { BannerProps } from '@nuxt/ui-pro'
+ import type { BannerProps } from '@nuxt/ui'
```

### From Nuxt UI

1. When upgrading from Nuxt UI v3, you simply need to update to v4:

```bash [pnpm]
pnpm add @nuxt/ui tailwindcss
```

```bash [yarn]
yarn add @nuxt/ui tailwindcss
```

```bash [npm]
npm install @nuxt/ui tailwindcss
```

```bash [bun]
bun add @nuxt/ui tailwindcss
```

## Changes from v3

After upgrading to Nuxt UI v4, please note the following important changes:

### Renamed ButtonGroup

The `ButtonGroup` component has been renamed to [`FieldGroup`](/docs/components/field-group):

```diff
<template>
- <UButtonGroup>
+ <UFieldGroup>
    <UButton label="Button" />
    <UButton icon="i-lucide-chevron-down" />
+ </UFieldGroup>
- </UButtonGroup>
</template>
```

### Renamed PageMarquee

The `PageMarquee` component has been renamed to [`Marquee`](/docs/components/marquee):

```diff
<template>
- <UPageMarquee :items="items" />
+ <UMarquee :items="items" />
</template>
```

### Removed PageAccordion

The `PageAccordion` component has been removed in favor of [`Accordion`](/docs/components/accordion):

```diff
<template>
- <UPageAccordion
+ <UAccordion
    :items="items"
+   :unmount-on-hide="false"
+   :ui="{ trigger: 'text-base', body: 'text-base text-muted' }"
  />
</template>
```

> [!NOTE]
> 
> The `PageAccordion` component was a wrapper that set `unmount-on-hide` to `false` and customized the `ui` prop.

### Renamed model modifiers

The `modelModifiers` shape used by [`Input`](/docs/components/input), [`InputNumber`](/docs/components/input-number) and [`Textarea`](/docs/components/textarea) has changed in v4:

1. The `nullify` modifier was renamed to `nullable` (it converts empty/blank values to `null`).
2. A new `optional` modifier was added (it converts empty/blank values to `undefined`).

```diff
- <UInput v-model.nullify="value" />
+ <UInput v-model.nullable="value" />
```

```diff
- <UTextarea v-model="value" :model-modifiers="{ nullify: true }" />
+ <UTextarea v-model="value" :model-modifiers="{ nullable: true }" />
```

Use `nullable` when you want empty values as `null`, and `optional` when you prefer `undefined` for absent values.

### Changes to Form component

The `Form` component has been improved in v4 with better state management and nested form handling. Here are the key changes you need to be aware of:

1. Schema **transformations will only** be applied to the **@submit data** and will no longer mutate the form's state. This provides better predictability and prevents unexpected state mutations.
2. **Nested forms must be enabled explicitly** using the `nested` prop. This makes the component behavior more explicit and prevents accidental nested form creation.
3. **Nested forms should now provide a name** prop (similar to `UFormField`) and will automatically inherit their state from their parent form.

```diff
<template>
  <UForm :state="state" :schema="schema" @submit="onSubmit">
    <UFormField label="Customer" name="customer">
      <UInput v-model="state.customer" placeholder="Wonka Industries" />
    </UFormField>

    <div v-for="(item, index) in state.items" :key="index">
      <UForm
-       :state="item"
+       :name="`items.${index}`"
        :schema="itemSchema"
+       nested
      >
        <UFormField :label="!index ? 'Description' : undefined" name="description">
          <UInput v-model="item.description" />
        </UFormField>
        <UFormField :label="!index ? 'Price' : undefined" name="price">
          <UInput v-model="item.price" type="number" />
        </UFormField>
      </UForm>
    </div>
  </UForm>
</template>
```

### Removed deprecated utilities

Some **Nuxt Content utilities** that were previously available in Nuxt UI Pro have been **removed** in v4:

- `findPageBreadcrumb`
- `findPageHeadline`

These are now fully provided by Nuxt Content. Make sure to update your imports and usage accordingly.

```diff
- import { findPageHeadline } from '@nuxt/ui-pro/utils/content'
+ import { findPageHeadline } from '@nuxt/content/utils'

- import { findPageBreadcrumb } from '@nuxt/ui-pro/utils/content'
+ import { findPageBreadcrumb } from '@nuxt/content/utils'
```

### AI SDK v5 migration (optional)

This section only applies if you're using the AI SDK and chat components (`ChatMessage`, `ChatMessages`, `ChatPrompt`, `ChatPromptSubmit`, `ChatPalette`). If you're not using AI features, you can skip this section.

1. Update `@ai-sdk/vue` and `ai` dependencies in your `package.json` and add [Comark](https://comark.dev) (replaces `@nuxtjs/mdc` for rendering AI responses as streaming Markdown):

**Nuxt:**

```diff
{
  "dependencies": {
-   "@ai-sdk/vue": "^1.2.x",
+   "@ai-sdk/vue": "^2.0.x",
-   "ai": "^4.3.x",
+   "ai": "^5.0.x",
+   "@comark/nuxt": "latest"
  }
}
```

Add `@comark/nuxt` to your modules:

```diff [nuxt.config.ts]
export default defineNuxtConfig({
  modules: [
    '@nuxt/ui',
+   '@comark/nuxt'
  ]
})
```

**Vue:**

```diff
{
  "dependencies": {
-   "@ai-sdk/vue": "^1.2.x",
+   "@ai-sdk/vue": "^2.0.x",
-   "ai": "^4.3.x",
+   "ai": "^5.0.x",
+   "@comark/vue": "latest"
  }
}
```

1. `useChat` composable has been replaced with the new `Chat` class:

```diff
<script setup lang="ts">
- import { useChat } from '@ai-sdk/vue'
+ import { Chat } from '@ai-sdk/vue'
+ import type { UIMessage } from 'ai'

- const { messages, input, handleSubmit, status, error, reload, setMessages } = useChat()
+ const messages: UIMessage[] = []
+ const input = ref('')
+
+ const chat = new Chat({
+   messages
+ })
+
+ function handleSubmit() {
+   chat.sendMessage({ text: input.value })
+   input.value = ''
+ }
</script>
```

1. Messages now use `parts` instead of `content`:

```diff
// When manually creating messages
- setMessages([{
+ messages.push({
  id: '1',
  role: 'user',
- content: 'Hello world'
+ parts: [{ type: 'text', text: 'Hello world' }]
- }])
+ })

// In templates
<template>
- <UChatMessage :content="message.content" />
+ <UChatMessage :parts="message.parts" />
</template>
```

1. Some methods have been renamed:

```diff
// Regenerate the last message
- reload()
+ chat.regenerate()

// Access chat state
- :messages="messages"
- :status="status"
+ :messages="chat.messages"
+ :status="chat.status"
```

1. Replace `<MDC>` with `<Comark>` for parts-based rendering. The `Comark` component is purpose-built for streaming Markdown, it incrementally renders tokens as they arrive from the AI. Use AI SDK helpers and the `isPartStreaming` utility from `@nuxt/ui/utils/ai`.

> [!NOTE]
> 
> When using the `highlight` plugin, add the following CSS to your stylesheet for dark mode support:
> 
> ```css [main.css]
> html.dark .shiki span {
>   color: var(--shiki-dark) !important;
>   background-color: var(--shiki-dark-bg) !important;
>   font-style: var(--shiki-dark-font-style) !important;
>   font-weight: var(--shiki-dark-font-weight) !important;
>   text-decoration: var(--shiki-dark-text-decoration) !important;
> }
> ```

**Nuxt:**

```vue
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart } from 'ai'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import highlight from '@comark/nuxt/plugins/highlight'
</script>

<template>
  <UChatMessages :messages="chat.messages" :status="chat.status">
    <template #content="{ message }">
      <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
        <UChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isPartStreaming(part)"
        >
          <Comark
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
        </UChatReasoning>

        <template v-else-if="isTextUIPart(part)">
          <Comark
            v-if="message.role === 'assistant'"
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
          <p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
            {{ part.text }}
          </p>
        </template>
      </template>
    </template>
  </UChatMessages>
</template>
```

**Vue:**

```vue
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart } from 'ai'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import { Comark } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'
</script>

<template>
  <UChatMessages :messages="chat.messages" :status="chat.status">
    <template #content="{ message }">
      <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
        <UChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isPartStreaming(part)"
        >
          <Comark
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
        </UChatReasoning>

        <template v-else-if="isTextUIPart(part)">
          <Comark
            v-if="message.role === 'assistant'"
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
          <p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
            {{ part.text }}
          </p>
        </template>
      </template>
    </template>
  </UChatMessages>
</template>
```

> [!NOTE]
> See: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
> 
> For more details on AI SDK v5 changes, review the **official AI SDK v5 migration guide**.

> [!TIP]
> See: https://github.com/nuxt/ui/pull/4698
> 
> View all changes from AI SDK v4 to v5 **in the upgrade PR** for a detailed migration reference.


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
