Nuxt UI is the component library the Nuxt team maintains: buttons, inputs, menus and dialogs that already look right, behave right on a keyboard and work with a screen reader, so you can spend your time on the parts of the app that are yours. Version 3.0.0 came out on 12 March 2025.
If you are starting a project, use v3: the steps below install it in a Nuxt 3 project and in a plain Vite app, then add a validated form and your own primary colour. If you maintain a v2 app, read the upgrade note in the next section first.
What shipped in Nuxt UI 3.0
The announcement calls v3 a complete redesign, more than 1500 commits in the making. It ships 54 core components and 42 Prose components for rendered Markdown. Three decisions define the release:
- The accessible primitives underneath are now Reka UI.
- The styling is Tailwind CSS v4, with a theme you edit in
app.config.ts. - The whole library works in any Vue project, not only in Nuxt.
Nuxt UI Pro, the add-on with 50 more components for landing pages, documentation sites and dashboards, remains a separate, paid licence. The news in this release is that Pro now works in a plain Vue project as well, through @nuxt/ui-pro/vite.
From Headless UI to Reka UI
Version 2 built its interactive components on Headless UI. Version 3 builds them on Reka UI 2, the unstyled Vue component library that was known as Radix Vue. Unstyled means Reka UI handles behaviour and leaves appearance to Nuxt UI.
The announcement lists what that buys: keyboard navigation, ARIA attributes (the roles, states and labels a screen reader announces) set automatically, focus management and screen reader support, for every menu, dialog, select, tabs and tooltip in the set. If you have ever written a dropdown by hand and then discovered it cannot be opened with the arrow keys, this is the part of the release to be glad about.
Tailwind CSS v4 and Tailwind Variants
Nuxt UI 3 moves to Tailwind CSS v4, which you configure in CSS rather than in a JavaScript file. The announcement credits the switch with faster builds and smaller CSS. Tailwind's own v4.0 release post gives the figures: full builds up to 5 times faster than v3, and incremental builds over 100 times faster.
Component styles are written with Tailwind Variants, a small library that describes a component as one object. The object lists the component's parts, which it calls slots (the elements that receive classes, not Vue's <slot>), and its variants, such as size, color and variant. You will use that shape when you restyle a component in the last section.
Colour aliases and semantic CSS variables
On top of Tailwind sits a design system with seven colour aliases: primary, secondary, success, info, warning, error and neutral. Each alias points at a Tailwind palette. Components with a color prop take an alias, as in color="primary" or color="error", so pointing primary at another palette changes every primary button and badge at once.
For your own markup there are semantic CSS variables for text, backgrounds and borders. They are named for their job rather than their colour: --ui-text-muted, --ui-bg-elevated, --ui-border and others. Each has a light and a dark value. Use them with the Tailwind v4 syntax for a CSS variable, text-(--ui-text-muted) or bg-(--ui-bg-elevated), and dark mode follows without a second dark: class.
Install in a Nuxt 3 project
This section uses the Nuxt 3 layout, with assets/ and pages/ at the project root. A new project can skip these steps: npx nuxi@latest init my-app -t ui creates one with Nuxt UI set up. In an existing project, add the package:
npm install @nuxt/ui
If you use pnpm, either set shamefully-hoist=true in .npmrc or install tailwindcss in the project root yourself, because pnpm's strict node_modules layout otherwise hides it from the library.
Next, register the module and a stylesheet in the Nuxt config:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
})
Create that stylesheet. It imports Tailwind, then the library:
@import "tailwindcss" theme(static);
@import "@nuxt/ui";
The theme(static) part is required with Nuxt UI 3.0.0. Tailwind 4.0.8 stopped emitting theme variables that your own classes do not use, and the library needs the full set.
Finally, wrap the app in UApp. It provides the global config, and toasts, tooltips and programmatic overlays (such as a modal opened from code) need it:
<template>
<UApp>
<NuxtPage />
</UApp>
</template>
Install in a plain Vite project
Without Nuxt, the library plugs in through a Vite plugin and a Vue plugin. Add the Vite plugin next to Vue's:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [vue(), ui()],
})
Then import the stylesheet and register a router and the Vue plugin in your entry file:
import './assets/main.css'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({ routes: [], history: createWebHistory() })
app.use(router)
app.use(ui)
app.mount('#app')
src/assets/main.css holds the same two @import lines as the Nuxt version, and App.vue wraps <RouterView /> in <UApp> instead of <NuxtPage />.
The router is part of the official setup because components that take a to prop, such as ULink and UButton, render their links through it. With pnpm and no shamefully-hoist, a Vite app needs vue-router and @unhead/vue installed in the project root, next to tailwindcss.
The Vite plugin also sets up auto-imports, so the library's components and composables (UButton, useToast) work without an import line, as they do in Nuxt. It writes their types to auto-imports.d.ts and components.d.ts: add both files to .gitignore, and to include in tsconfig.app.json so TypeScript sees them.
Add a button and a validated form
Components use a U prefix. A button takes a colour alias, a variant (solid, outline, soft, subtle, ghost or link) and a size from xs to xl:
<template>
<UButton color="primary" variant="solid" size="md">
Save changes
</UButton>
<UButton color="neutral" variant="ghost" size="md">
Cancel
</UButton>
</template>
Forms are where a library earns its keep. UForm takes a reactive state object and a schema from a validation library. No validator is bundled, so install the one you use. This sign-up form uses Zod:
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.string().email('Enter a valid email address'),
password: z.string().min(8, 'Use at least 8 characters'),
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ email: undefined, password: undefined })
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Account created', color: 'success' })
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<UButton type="submit">Create account</UButton>
</UForm>
</template>
What each part does:
- The
nameon eachUFormFieldties it to a key in the schema, and that key's error message appears under the field. - A field is checked when the user leaves it, then again as they type. Submitting checks every field.
onSubmitruns only when the whole form passes.event.datais already validated and typed asSchema, the type Zod infers from the schema, so there is no second check to write.useToast()andreactiveare auto-imported in Nuxt. In the Vite setup, importreactivefromvue.
Change colours and component styles
The default primary is green and neutral is slate. In Nuxt, change them in app.config.ts, which is read at runtime, so no rebuild is needed:
export default defineAppConfig({
ui: {
colors: {
primary: 'blue',
neutral: 'zinc',
},
},
})
In the Vite setup the same object goes into the plugin options in vite.config.ts: ui({ ui: { colors: { primary: 'blue', neutral: 'zinc' } } }).
The names are Tailwind palettes. To use your own palette, define all eleven shades from 50 to 950 in your stylesheet, for example @theme static { --color-brand-500: #2563eb; } plus the other ten, then set primary: 'brand'.
To restyle one component everywhere, add its theme under ui in the same file, in the Tailwind Variants shape: ui.button.slots.base takes classes for every button, and ui.button.defaultVariants.size sets the size a button gets when you pass none.