Migration to v3
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:
- Tailwind CSS v4: Migration from JavaScript to CSS-based configuration
- Reka UI: Replacing Headless UI as the underlying component library
- Tailwind Variants: New styling API for component variants
This guide provides step by step instructions to migrate your application from v2 to v3.
Migrate your project
Update Tailwind CSS
Tailwind CSS v4 introduces significant changes to its configuration approach. The official Tailwind upgrade tool will help automate most of the migration process.
- Create a
main.cssfile and import it in yournuxt.config.tsfile:
@import "tailwindcss";
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
- Run the Tailwind CSS upgrade tool:
npx @tailwindcss/upgrade
Update Nuxt UI
- Install the latest version of the package:
pnpm add @nuxt/ui
yarn add @nuxt/ui
npm install @nuxt/ui
bun add @nuxt/ui
- Import it in your CSS:
@import "tailwindcss";
@import "@nuxt/ui";
- Wrap your app with the App component:
<template>
<UApp>
<NuxtPage />
</UApp>
</template>
Changes from v2
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.
Updated design system
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:
- The
graycolor has been renamed toneutral
<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>
- The
gray,blackandwhitein thecolorprops have been removed in favor ofneutral:
- <UButton color="black" />
+ <UButton color="neutral" />
- <UButton color="gray" />
+ <UButton color="neutral" variant="subtle" />
- <UButton color="white" />
+ <UButton color="neutral" variant="outline" />
- You can no longer use Tailwind CSS colors in the
colorprops, use the new aliases instead:
- <UButton color="red" />
+ <UButton color="error" />
- The color configuration in
app.config.tshas been moved into acolorsobject:
export default defineAppConfig({
ui: {
- primary: 'green',
- gray: 'cool'
+ colors: {
+ primary: 'green',
+ neutral: 'slate'
+ }
}
})
Updated theming system
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.
- Update your
app.config.tsto 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'
+ }
}
}
})
- Update your
uiprops to override each component's slots using their new theme:
<template>
- <UButton :ui="{ font: 'font-bold' }" />
+ <UButton :ui="{ base: 'font-bold' }" />
</template>
Renamed components
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 |
Meter | Removed |
Notification | Toast |
Radio | Removed (use RadioGroup instead) |
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) |
Changed components
In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:
- The
linksandoptionsprops have been renamed toitemsfor consistency:
<template>
- <USelect :options="countries" />
+ <USelect :items="countries" />
- <UHorizontalNavigation :links="links" />
+ <UNavigationMenu :items="links" />
</template>
Breadcrumb, HorizontalNavigation, InputMenu, RadioGroup, Select, SelectMenu, VerticalNavigation.- The
clickfield in different components has been removed in favor of the native VueonClickevent:
<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.- The global
Modals,SlideoversandNotificationscomponents have been removed in favor of the App component:
<template>
+ <UApp>
+ <NuxtPage />
+ </UApp>
- <UModals />
- <USlideovers />
- <UNotifications />
</template>
- The
v-model:opendirective anddefault-openprop 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.- The default slot is now used for the trigger and the content goes inside the
#contentslot (you don't need to use av-model:opendirective 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.- A
#header,#bodyand#footerslots have been added inside the#contentslot like:
<template>
- <UModal>
+ <UModal title="Title" description="Description">
- <div class="p-4">
+ <template #body>
<Placeholder class="h-48" />
+ </template>
- </div>
</UModal>
</template>
Modal, Slideover.- The
prevent-closeprop has been removed in favor of thedismissibleprop:
<template>
- <UModal prevent-close />
+ <UModal :dismissible="false" />
</template>
Modal, Slideover.- The
Paginationcomponentv-modeldirective has been renamed tov-model:page:
<template>
- <UPagination v-model="page" />
+ <UPagination v-model:page="page" />
</template>
- The
changeevent now emits the nativechangeevent, not the new value, which is now emitted in theupdate:modelValueevent:
<template>
- <USelectMenu v-model="country" :items="countries" @change="console.log(newVal)" />
+ <USelectMenu v-model="country" :items="countries" @update:modelValue="console.log(newVal)" />
</template>
Select, SelectMenu, RadioGroup.- The
SelectMenucomponentsearchableprop has been renamed tosearch-inputand now defaults totrue. To preserve v2 behavior (no search input):
<template>
- <USelectMenu :items="items" />
+ <USelectMenu :search-input="false" :items="items" />
</template>
- The
Accordioncomponent has been redesigned. Themultipleprop has been replaced by thetypeprop (defaults tosingle):
<template>
- <UAccordion multiple :items="items" />
+ <UAccordion type="multiple" :items="items" />
</template>
- The
Accordioncomponentdefault-openprop anddefaultOpenitem property have been removed. State is now controlled usingdefault-value(uncontrolled) orv-model(controlled):
<template>
- <UAccordion default-open multiple :items="items" />
+ <UAccordion
+ type="multiple"
+ :default-value="['0', '1']"
+ :items="items"
+ />
</template>
- The
Accordioncomponent#itemslot has been removed in favor of#contentand#body:
<template>
- <template #item="{ item }">
- {{ item.content }}
- </template>
+ <template #content="{ item }">
+ {{ item.content }}
+ </template>
</template>
#leading, #trailing, #body).- The
Accordioncomponentunmountprop has been renamed tounmount-on-hideand now defaults totrue. To preserve v2 behavior (keep content mounted), use:unmount-on-hide="false":
<template>
- <UAccordion :items="items" />
+ <UAccordion :unmount-on-hide="false" :items="items" />
</template>
- The
Tablecomponent now uses TanStack Table under the hood. Therowsprop has been renamed todata:
<template>
- <UTable :rows="rows" />
+ <UTable :data="data" />
</template>
- The
Tablecomponent columns definition is now explicit and semantic:
<script setup lang="ts">
const columns = [{
- label: 'Status',
- key: 'status'
+ header: 'Status',
+ accessorKey: 'status'
}]
</script>
- The
Tablecomponent row cell slot names have been changed from<column-accessorKey>-datato<column-accessorKey>-cell:
<template>
- <template #column-data="{ row }">
+ <template #column-cell="{ row }">
</template>
- The
Tabscomponent#itemslot has been removed in favor of#content:
<template>
- <template #item="{ item }">
+ <template #content="{ item }">
</template>
- The
Tabscomponentdefault-indexprop has been removed in favor ofdefault-value:
<template>
- <UTabs :default-index="0" :items="tabs" />
+ <UTabs :default-value="0" :items="tabs" />
</template>
- The
Tabscomponentunmountprop has been renamed tounmount-on-hideand now defaults totrue. To preserve v2 behavior where content stayed mounted:
<template>
- <UTabs :items="tabs" />
+ <UTabs :unmount-on-hide="false" :items="tabs" />
</template>
- The
Alertcomponentclose-buttonprop has been replaced by thecloseprop:
<template>
- <UAlert :close-button="{ icon: 'i-heroicons-x-mark', variant: 'link' }" />
+ <UAlert :close="{ icon: 'i-lucide-x', variant: 'link' }" />
</template>
- The
Alertcomponentcloseevent has been replaced by theupdate:openevent:
<template>
- <UAlert @close="isOpen = false" />
+ <UAlert @update:open="isOpen = false" />
</template>
- The
Alertcomponent#iconand#avatarslots have been replaced by a single#leadingslot:
<template>
- <UAlert>
- <template #icon>
- <UIcon name="i-heroicons-command-line" />
- </template>
- </UAlert>
+ <UAlert>
+ <template #leading>
+ <UIcon name="i-lucide-terminal" />
+ </template>
+ </UAlert>
</template>
- The
Formcomponent now always validates on submit. Thevalidate-onprop only controls which input events trigger validation. Pass an empty array to validate only on submit:
<template>
- <UForm :validate-on="['submit']" />
+ <UForm :validate-on="[]" />
</template>
- Form components now use
inline-flexinstead ofblocklayout, which means they no longer expand to full width by default. Addw-fullmanually with theclassprop or configure it globally in yourapp.config.ts:
export default defineAppConfig({
ui: {
input: { slots: { root: 'w-full' } },
inputMenu: { slots: { root: 'w-full' } },
textarea: { slots: { root: 'w-full' } },
select: { slots: { base: 'w-full' } },
selectMenu: { slots: { base: 'w-full' } }
}
})
Input, InputMenu, Textarea, Select, SelectMenu.- The
popperprop has been replaced bycontentfor positioning:
<template>
- <UTooltip :popper="{ placement: 'top' }" />
+ <UTooltip :content="{ side: 'top' }" />
- <USelectMenu :popper="{ placement: 'bottom-start' }" />
+ <USelectMenu :content="{ side: 'bottom', align: 'start' }" />
</template>
Tooltip, Popover, DropdownMenu, ContextMenu, SelectMenu, InputMenu.- The
Tooltipcomponentshortcutsprop has been renamed tokbdsandpreventtodisabled:
<template>
- <UTooltip text="Open" :shortcuts="['⌘', 'O']" />
+ <UTooltip text="Open" :kbds="['meta', 'O']" />
</template>
- The
Popovercomponent#panelslot has been renamed to#content:
<template>
<UPopover>
<UButton label="Open" />
- <template #panel>
+ <template #content>
<div class="p-4">Content</div>
</template>
</UPopover>
</template>
- The
ContextMenucomponent has been completely redesigned. It now uses items and has a proper trigger/content structure:
<template>
- <UContextMenu v-model="isOpen" :virtual-element="virtualElement" />
+ <UContextMenu :items="items">
+ <div>Right-click me</div>
+ </UContextMenu>
</template>
- The
Progresscomponentvalueprop has been replaced bymodel-valueandindicatorbystatus:
<template>
- <UProgress :value="50" indicator />
+ <UProgress :model-value="50" status />
</template>
- The
Carouselcomponentindicatorsprop has been renamed todots:
<template>
- <UCarousel :items="items" indicators />
+ <UCarousel :items="items" dots />
</template>
Carousel component now uses Embla Carousel under the hood.- The
helpprop/property has been renamed todescription:
<template>
- <UCheckbox label="Remember me" help="Save my login details" />
+ <UCheckbox label="Remember me" description="Save my login details" />
</template>
<script setup lang="ts">
const items = [{
label: 'Option 1',
- help: 'Description for option 1'
+ description: 'Description for option 1'
}]
</script>
Checkbox, RadioGroup.- The
Breadcrumbcomponentdividerprop has been renamed toseparator-iconand#dividerslot to#separator:
<template>
- <UBreadcrumb :links="links" divider="i-lucide-arrow-right" />
+ <UBreadcrumb :items="items" separator-icon="i-lucide-arrow-right" />
</template>
- The
Avatarcomponent chip props (chip-color,chip-position,chip-text) have been consolidated into a singlechipprop:
<template>
- <UAvatar src="..." chip-color="green" chip-position="top-right" chip-text="" />
+ <UAvatar src="..." :chip="{ color: 'success', position: 'top-right' }" />
</template>
- The
Buttoncomponentpaddedandtruncateprops have been removed. Usesquareinstead of:padded="false":
<template>
- <UButton :padded="false" />
+ <UButton square />
</template>
- The
Chipcomponentshowprop is now a model (v-model:show):
<template>
- <UChip :show="isVisible" />
+ <UChip v-model:show="isVisible" />
</template>
- The
CommandPalettecomponentgroupsprop structure has changed. Each group now has anitemsarray and usesonSelectinstead ofclick:
<script setup lang="ts">
const groups = [{
id: 'actions',
label: 'Actions',
- commands: [{ id: 'new', label: 'New file' }]
+ items: [{ id: 'new', label: 'New file' }]
}]
</script>
Changed composables
- The
useToast()composabletimeoutprop has been renamed toduration:
<script setup lang="ts">
const toast = useToast()
- toast.add({ title: 'Invitation sent', timeout: 0 })
+ toast.add({ title: 'Invitation sent', duration: 0 })
</script>
- The
useModalanduseSlideovercomposables have been removed in favor of a more genericuseOverlaycomposable:
Some important differences:
- The
useOverlaycomposable is now used to create overlay instances - Overlays that are opened, can be awaited for their result
- Overlays can no longer be close using
modal.close()orslideover.close(), rather, they close automatically: either when acloseevent is fired explicitly from the opened component OR when the overlay closes itself (clicking on backdrop, pressing the ESC key, etc) - To capture the return value in the parent component you must explicitly emit a
closeevent 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>
Changed form validation
- The error object property for targeting form fields has been renamed from
pathtoname:
<script setup lang="ts">
function 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>