Nuxt UI v3 is a new major version rebuilt from the ground up, introducing a modern architecture with significant performance improvements and an enhanced developer experience. This major release includes several breaking changes alongside powerful new features and capabilities:
This guide provides step by step instructions to migrate your application from v2 to v3.
Tailwind CSS v4 introduces significant changes to its configuration approach. The official Tailwind upgrade tool will help automate most of the migration process.
main.css file and import it in your nuxt.config.ts file:@import "tailwindcss";
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
npx @tailwindcss/upgrade
pnpm add @nuxt/ui
yarn add @nuxt/ui
npm install @nuxt/ui
bun add @nuxt/ui
@import "tailwindcss";
@import "@nuxt/ui";
<template>
<UApp>
<NuxtPage />
</UApp>
</template>
Now that you have updated your project, you can start migrating your code. Here's a comprehensive list of all the breaking changes in Nuxt UI v3.
In Nuxt UI v2, we had a mix between a design system with primary, gray, error aliases and all the colors from Tailwind CSS. We've replaced it with a proper design system with 7 color aliases:
| Color | Default | Description |
|---|---|---|
primary | green | Main brand color, used as the default color for components. |
secondary | blue | Secondary color to complement the primary color. |
success | green | Used for success states. |
info | blue | Used for informational states. |
warning | yellow | Used for warning states. |
error | red | Used for form error validation states. |
neutral | slate | Neutral color for backgrounds, text, etc. |
This change introduces several breaking changes that you need to be aware of:
gray color has been renamed to neutral<template>
- <p class="text-gray-500 dark:text-gray-400" />
+ <p class="text-neutral-500 dark:text-neutral-400" />
</template>
<template>
- <p class="text-gray-500 dark:text-gray-400" />
+ <p class="text-muted" />
- <p class="text-gray-900 dark:text-white" />
+ <p class="text-highlighted" />
</template>
gray, black and white in the color props have been removed in favor of neutral:- <UButton color="black" />
+ <UButton color="neutral" />
- <UButton color="gray" />
+ <UButton color="neutral" variant="subtle" />
- <UButton color="white" />
+ <UButton color="neutral" variant="outline" />
color props, use the new aliases instead:- <UButton color="red" />
+ <UButton color="error" />
app.config.ts has been moved into a colors object:export default defineAppConfig({
ui: {
- primary: 'green',
- gray: 'cool'
+ colors: {
+ primary: 'green',
+ neutral: 'slate'
+ }
}
})
Nuxt UI components are now styled using the Tailwind Variants API, which makes all the overrides you made using the app.config.ts and the ui prop obsolete.
app.config.ts to override components with their new theme:export default defineAppConfig({
ui: {
button: {
- font: 'font-bold',
- default: {
- size: 'md',
- color: 'primary'
- }
+ slots: {
+ base: 'font-medium'
+ },
+ defaultVariants: {
+ size: 'md',
+ color: 'primary'
+ }
}
}
})
ui props to override each component's slots using their new theme:<template>
- <UButton :ui="{ font: 'font-bold' }" />
+ <UButton :ui="{ base: 'font-bold' }" />
</template>
We've renamed some Nuxt UI components to align with the Reka UI naming convention:
| v2 | v3 |
|---|---|
Divider | Separator |
Dropdown | DropdownMenu |
FormGroup | FormField |
Range | Slider |
Toggle | Switch |
Notification | Toast |
VerticalNavigation | NavigationMenu with orientation="vertical" |
HorizontalNavigation | NavigationMenu with orientation="horizontal" |
Here are the Nuxt UI Pro components that have been renamed or removed:
| v1 | v3 |
|---|---|
BlogList | BlogPosts |
ColorModeToggle | ColorModeSwitch |
DashboardCard | Removed (use PageCard instead) |
DashboardLayout | DashboardGroup |
DashboardModal | Removed (use Modal instead) |
DashboardNavbarToggle | DashboardSidebarToggle |
DashboardPage | Removed |
DashboardPanelContent | Removed (use #body slot instead) |
DashboardPanelHandle | DashboardResizeHandle |
DashboardSection | Removed (use PageCard instead) |
DashboardSidebarLinks | Removed (use NavigationMenu instead) |
DashboardSlideover | Removed (use Slideover instead) |
FooterLinks | Removed (use NavigationMenu instead) |
HeaderLinks | Removed (use NavigationMenu instead) |
LandingCard | Removed (use PageCard instead) |
LandingCTA | PageCTA |
LandingFAQ | Removed (use Accordion instead) |
LandingGrid | Removed (use PageGrid instead) |
LandingHero | Removed (use PageHero instead) |
LandingLogos | PageLogos |
LandingSection | PageSection |
LandingTestimonial | Removed (use PageCard instead) |
NavigationAccordion | ContentNavigation |
NavigationLinks | ContentNavigation |
NavigationTree | ContentNavigation |
PageError | Error |
PricingCard | PricingPlan |
PricingGrid | PricingPlans |
PricingSwitch | Removed (use Switch or Tabs instead) |
In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:
links and options props have been renamed to items for consistency:<template>
- <USelect :options="countries" />
+ <USelect :items="countries" />
- <UHorizontalNavigation :links="links" />
+ <UNavigationMenu :items="links" />
</template>
Breadcrumb, HorizontalNavigation, InputMenu, RadioGroup, Select, SelectMenu, VerticalNavigation.click field in different components has been removed in favor of the native Vue onClick event:<script setup lang="ts">
const items = [{
label: 'Edit',
- click: () => {
+ onClick: () => {
console.log('Edit')
}
}]
</script>
Toast component as well as all component that have items links like NavigationMenu, DropdownMenu, CommandPalette, etc.Modals, Slideovers and Notifications components have been removed in favor the App component:<template>
+ <UApp>
+ <NuxtPage />
+ </UApp>
- <UModals />
- <USlideovers />
- <UNotifications />
</template>
v-model:open directive and default-open prop are now used to control visibility:<template>
- <UModal v-model="open" />
+ <UModal v-model:open="open" />
</template>
ContextMenu, Modal and Slideover and enables controlling visibility for InputMenu, Select, SelectMenu and Tooltip.#content slot (you don't need to use a v-model:open directive with this method):<script setup lang="ts">
- const open = ref(false)
</script>
<template>
- <UButton label="Open" @click="open = true" />
- <UModal v-model="open">
+ <UModal>
+ <UButton label="Open" />
+ <template #content>
<div class="p-4">
<Placeholder class="h-48" />
</div>
+ </template>
</UModal>
</template>
Modal, Popover, Slideover, Tooltip.#header, #body and #footer slots have been added inside the #content slot like the Card component:<template>
- <UModal>
+ <UModal title="Title" description="Description">
- <div class="p-4">
+ <template #body>
<Placeholder class="h-48" />
+ </template>
- </div>
</UModal>
</template>
Modal, Slideover.useToast() composable timeout prop has been renamed to duration:<script setup lang="ts">
const toast = useToast()
- toast.add({ title: 'Invitation sent', timeout: 0 })
+ toast.add({ title: 'Invitation sent', duration: 0 })
</script>
useModal and useSlideover composables have been removed in favor of a more generic useOverlay composable:Some important differences:
useOverlay composable is now used to create overlay instancesmodal.close() or slideover.close(), rather, they close automatically: either when a close event is fired explicitly from the opened component OR when the overlay closes itself (clicking on backdrop, pressing the ESC key, etc)close event with the desired value<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
- modal.open(ModalExampleComponent)
+ const modal = overlay.create(ModalExampleComponent)
</script>
Props are now passed through a props attribute:
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
const count = ref(0)
- modal.open(ModalExampleComponent, {
- count: count.value
- })
+ const modal = overlay.create(ModalExampleComponent, {
+ props: {
+ count: count.value
+ }
+ })
</script>
Closing a modal is now done through the close event. The modal.open method now returns an instance that can be used to await for the result of the modal whenever the modal is closed:
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
+ const modal = overlay.create(ModalExampleComponent)
- function openModal() {
- modal.open(ModalExampleComponent, {
- onSuccess() {
- toast.add({ title: 'Success!' })
- }
- })
- }
+ async function openModal() {
+ const instance = modal.open(ModalExampleComponent, {
+ count: count.value
+ })
+
+ const result = await instance.result
+
+ if (result) {
+ toast.add({ title: 'Success!' })
+ }
+ }
</script>
path to name:<script setup lang="ts">
const validate = (state: any): FormError[] => {
const errors = []
if (!state.email) {
errors.push({
- path: 'email',
+ name: 'email',
message: 'Required'
})
}
if (!state.password) {
errors.push({
- path: 'password',
+ name: 'password',
message: 'Required'
})
}
return errors
}
</script>