18 KiB
Nuxt UI Reference
Last Updated: 2025-11 (v4.1.0)
Check: @nuxt/ui in package.json
Nuxt UI is a component library built on Tailwind CSS that provides pre-built, accessible UI components for Nuxt applications.
Version Detection
Check package.json to determine which version is installed:
- v4.x - Latest version (Nuxt 4 required, Tailwind v4, breaking changes from v3)
- v3.x - Previous stable (Nuxt 3, Tailwind v3)
- v2.x - Legacy version (deprecated)
For v4-specific features and migration, always verify with official docs: https://ui.nuxt.com
Installation & Setup
v4 Setup (Current)
Prerequisites:
- Nuxt 4+
- Tailwind CSS v4
pnpm add @nuxt/ui
Required Configuration:
- nuxt.config.ts:
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
// Optional: Configure color theme
colorMode: {
preference: "system", // 'light' | 'dark' | 'system'
},
})
- app.vue - UApp Wrapper (Required):
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
- assets/css/main.css - Tailwind v4 Import:
@import "tailwindcss";
- app.config.ts - Color Configuration:
export default defineAppConfig({
ui: {
colors: {
primary: "blue",
neutral: "slate",
},
},
})
v3 Setup (Previous Version)
pnpm add @nuxt/ui
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
})
/* assets/css/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Breaking Changes
v3 → v4 Migration
Component Renames:
UButtonGroup→UFieldGroupUFormGroup→UFieldGroupUVerticalNavigation→UNavigationTree
Modal/Popover/Slideover Structure Changes:
Major structural changes - trigger button now goes inside component, content uses #content slot:
<!-- v3 -->
<UButton @click="open = true">Open</UButton>
<UModal v-model="open">
<div class="p-4">Content</div>
</UModal>
<!-- v4 -->
<UModal>
<UButton>Open</UButton>
<template #content>
<template #header>Title</template>
<template #body>Content</template>
<template #footer>Footer</template>
</template>
</UModal>
Composable Changes:
useModal()→useOverlay()- Overlays now close automatically on close events (no manual
.close()needed)
Color System:
Configure colors in app.config.ts instead of component props:
// app.config.ts (v4)
export default defineAppConfig({
ui: {
colors: {
primary: "blue", // Changed from 'primary' prop on components
neutral: "slate",
},
},
})
Tailwind v4 Requirement:
- Must use
@import "tailwindcss"in main.css - Tailwind config now uses CSS-based configuration
For complete migration details, always check: https://ui.nuxt.com/getting-started/migration
v2 → v3 Migration
Major overhaul with many breaking changes. Recommend checking official migration guide for projects still on v2.
Common Components
Forms
UInput
<template>
<UInput
v-model="email"
type="email"
placeholder="Enter your email"
icon="i-heroicons-envelope"
/>
</template>
<script setup lang="ts">
const email = ref("")
</script>
UTextarea
<template>
<UTextarea
v-model="description"
placeholder="Enter description..."
:rows="4"
/>
</template>
<script setup lang="ts">
const description = ref("")
</script>
USelect
<template>
<USelect
v-model="selectedOption"
:options="options"
placeholder="Select an option"
/>
</template>
<script setup lang="ts">
const selectedOption = ref("")
const options = [
{ label: "Option 1", value: "opt1" },
{ label: "Option 2", value: "opt2" },
]
</script>
UCheckbox & URadio
<template>
<div>
<UCheckbox v-model="agreed" label="I agree to terms" />
<URadioGroup v-model="plan">
<URadio value="free" label="Free" />
<URadio value="pro" label="Pro" />
</URadioGroup>
</div>
</template>
<script setup lang="ts">
const agreed = ref(false)
const plan = ref("free")
</script>
UFieldGroup (v4) / UFormGroup (v3)
<template>
<!-- v4 -->
<UFieldGroup label="Email" description="Your email address" required>
<UInput v-model="email" type="email" />
</UFieldGroup>
<!-- v3 -->
<UFormGroup label="Email" help="Your email address" required>
<UInput v-model="email" type="email" />
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref("")
</script>
Buttons
UButton
<template>
<div class="flex gap-2">
<!-- v4: Color defined in app.config.ts -->
<UButton>Primary Button</UButton>
<UButton variant="outline">Outline</UButton>
<UButton variant="ghost">Ghost</UButton>
<UButton size="sm" icon="i-heroicons-plus">Add</UButton>
<UButton loading>Loading...</UButton>
<UButton disabled>Disabled</UButton>
</div>
</template>
UButtonGroup (v3) / UFieldGroup (v4)
<template>
<!-- v3 -->
<UButtonGroup>
<UButton>One</UButton>
<UButton>Two</UButton>
<UButton>Three</UButton>
</UButtonGroup>
<!-- v4: Use flex utilities or UFieldGroup -->
<div
class="flex gap-0 [&>button]:rounded-none [&>button:first-child]:rounded-l [&>button:last-child]:rounded-r"
>
<UButton>One</UButton>
<UButton>Two</UButton>
<UButton>Three</UButton>
</div>
</template>
Overlays
UModal
<template>
<!-- v4: Trigger button inside Modal, content in #content slot -->
<UModal>
<UButton>Open Modal</UButton>
<template #content>
<template #header>
<h3>Modal Title</h3>
</template>
<template #body>
<p>Modal content goes here...</p>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline">Cancel</UButton>
<UButton @click="handleSubmit">Submit</UButton>
</div>
</template>
</template>
</UModal>
<!-- v3 (old structure) -->
<div>
<UButton @click="isOpen = true">Open Modal</UButton>
<UModal v-model="isOpen">
<UCard>
<template #header>
<h3>Modal Title</h3>
</template>
<p>Modal content goes here...</p>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline" @click="isOpen = false">Cancel</UButton>
<UButton @click="handleSubmit">Submit</UButton>
</div>
</template>
</UCard>
</UModal>
</div>
</template>
<script setup lang="ts">
function handleSubmit() {
// Handle submission
}
</script>
UPopover
<template>
<!-- v4: Similar structure to Modal -->
<UPopover>
<UButton>Open Popover</UButton>
<template #content>
<div class="p-4">
<p>Popover content</p>
</div>
</template>
</UPopover>
<!-- v3 (old structure) -->
<div>
<UButton @click="isPopoverOpen = true">Open Popover</UButton>
<UPopover v-model="isPopoverOpen">
<div class="p-4">
<p>Popover content</p>
</div>
</UPopover>
</div>
</template>
<script setup lang="ts">
// v4: No state management needed
// v3: const isPopoverOpen = ref(false)
</script>
UTooltip
<template>
<UTooltip text="Helpful tooltip">
<UButton icon="i-heroicons-information-circle" />
</UTooltip>
</template>
Navigation
ULink
<template>
<div>
<!-- Internal link -->
<ULink to="/about">About Us</ULink>
<!-- External link -->
<ULink to="https://example.com" target="_blank"> External Link </ULink>
<!-- With icon -->
<ULink to="/settings" icon="i-heroicons-cog"> Settings </ULink>
</div>
</template>
UTabs
<template>
<UTabs v-model="selectedTab" :items="tabs">
<template #account>
<div class="p-4">Account settings content</div>
</template>
<template #security>
<div class="p-4">Security settings content</div>
</template>
</UTabs>
</template>
<script setup lang="ts">
const selectedTab = ref("account")
const tabs = [
{ key: "account", label: "Account" },
{ key: "security", label: "Security" },
]
</script>
UBreadcrumb
<template>
<UBreadcrumb :items="breadcrumbs" />
</template>
<script setup lang="ts">
const route = useRoute()
const breadcrumbs = computed(() => [
{ label: "Home", to: "/" },
{ label: "Products", to: "/products" },
{ label: route.params.id as string },
])
</script>
Data Display
UCard
<template>
<UCard>
<template #header>
<h3>Card Title</h3>
</template>
<p>Card body content goes here.</p>
<template #footer>
<div class="flex justify-end gap-2">
<UButton>Action</UButton>
</div>
</template>
</UCard>
</template>
UTable
<template>
<UTable :columns="columns" :rows="users">
<template #name-data="{ row }">
<span class="font-medium">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UButton size="xs" @click="editUser(row)">Edit</UButton>
</template>
</UTable>
</template>
<script setup lang="ts">
const columns = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "actions", label: "Actions" },
]
const users = ref([
{ id: 1, name: "John Doe", email: "john@example.com" },
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
])
function editUser(user: any) {
// Handle edit
}
</script>
UBadge
<template>
<div class="flex gap-2">
<UBadge>Default</UBadge>
<UBadge variant="subtle">Subtle</UBadge>
<UBadge size="sm">Small</UBadge>
</div>
</template>
UAlert
<template>
<div class="space-y-2">
<UAlert
title="Success"
description="Your changes have been saved."
icon="i-heroicons-check-circle"
/>
<UAlert
title="Error"
description="Something went wrong."
variant="error"
icon="i-heroicons-x-circle"
/>
</div>
</template>
Feedback
UNotification (Toast)
<template>
<UButton @click="showToast">Show Toast</UButton>
</template>
<script setup lang="ts">
const toast = useToast()
function showToast() {
toast.add({
title: "Success",
description: "Your action was successful!",
timeout: 3000,
})
}
</script>
UProgress
<template>
<div>
<UProgress :value="progress" :max="100" />
<UButton @click="progress += 10">Increase</UButton>
</div>
</template>
<script setup lang="ts">
const progress = ref(30)
</script>
Form Validation
Using with Zod (Recommended)
<template>
<UForm :state="state" :schema="schema" @submit="onSubmit">
<UFieldGroup label="Email" name="email" required>
<UInput v-model="state.email" type="email" />
</UFieldGroup>
<UFieldGroup label="Password" name="password" required>
<UInput v-model="state.password" type="password" />
</UFieldGroup>
<UButton type="submit">Submit</UButton>
</UForm>
</template>
<script setup lang="ts">
import { z } from "zod"
const schema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})
const state = reactive({
email: "",
password: "",
})
async function onSubmit() {
// Form is validated, handle submission
console.log("Valid form data:", state)
}
</script>
Theming & Customization
Color Configuration (v4)
// app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: "blue",
secondary: "purple",
success: "green",
warning: "yellow",
error: "red",
neutral: "slate",
},
},
})
Component Styling
Override component styles using Tailwind classes:
<template>
<UButton
class="custom-button"
:ui="{
base: 'font-bold',
rounded: 'rounded-full',
size: { sm: 'px-4 py-2' },
}"
>
Custom Styled Button
</UButton>
</template>
Dark Mode
Nuxt UI automatically supports dark mode when @nuxtjs/color-mode is configured:
<template>
<div>
<!-- Components automatically adapt to dark mode -->
<UCard>
<p>This card adapts to light/dark mode</p>
</UCard>
<!-- Toggle dark mode -->
<UButton @click="toggleDark"> Toggle Dark Mode </UButton>
</div>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
function toggleDark() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark"
}
</script>
Nuxt UI-Specific Tailwind Configuration
When using Nuxt UI v4 with Tailwind v4, the Tailwind setup is automatically configured by the module. However, you may need custom configuration:
Custom Tailwind Classes with Nuxt UI
/* assets/css/main.css */
@import "tailwindcss";
/* Custom utilities that work with Nuxt UI */
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent;
}
}
Extending Nuxt UI Colors
// tailwind.config.ts
import type { Config } from "tailwindcss"
export default {
theme: {
extend: {
colors: {
// Add custom colors that integrate with Nuxt UI
brand: {
50: "#f0f9ff",
100: "#e0f2fe",
// ... more shades
900: "#0c4a6e",
},
},
},
},
} satisfies Config
Reference app.config.ts to use custom color in Nuxt UI:
// app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: "brand", // Use custom Tailwind color
},
},
})
Common Patterns
Loading States
<template>
<div>
<UButton :loading="isLoading" @click="handleAction"> Submit </UButton>
</div>
</template>
<script setup lang="ts">
const isLoading = ref(false)
async function handleAction() {
isLoading.value = true
try {
await $fetch("/api/action")
} finally {
isLoading.value = false
}
}
</script>
Confirmation Dialogs
<template>
<!-- v4 structure -->
<UModal>
<UButton>Delete</UButton>
<template #content>
<template #header>
<h3>Confirm Deletion</h3>
</template>
<template #body>
<p>Are you sure you want to delete this item?</p>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline">Cancel</UButton>
<UButton variant="error" @click="handleDelete">Delete</UButton>
</div>
</template>
</template>
</UModal>
</template>
<script setup lang="ts">
async function handleDelete() {
// Perform deletion
// Modal closes automatically
}
</script>
Data Tables with Actions
<template>
<UTable :columns="columns" :rows="items" :loading="pending">
<template #actions-data="{ row }">
<UDropdown :items="getActions(row)">
<UButton icon="i-heroicons-ellipsis-horizontal" variant="ghost" />
</UDropdown>
</template>
</UTable>
</template>
<script setup lang="ts">
const columns = [
{ key: "name", label: "Name" },
{ key: "status", label: "Status" },
{ key: "actions", label: "" },
]
const { data: items, pending } = await useFetch("/api/items")
function getActions(row: any) {
return [
[
{
label: "Edit",
icon: "i-heroicons-pencil",
click: () => editItem(row),
},
],
[
{
label: "Delete",
icon: "i-heroicons-trash",
click: () => deleteItem(row),
},
],
]
}
function editItem(item: any) {
// Handle edit
}
function deleteItem(item: any) {
// Handle delete
}
</script>
Icons
Nuxt UI v4 uses Iconify with the i- prefix:
<template>
<div>
<!-- Heroicons (default icon set) -->
<UButton icon="i-heroicons-home">Home</UButton>
<!-- Material Design Icons -->
<UButton icon="i-mdi-github">GitHub</UButton>
<!-- Font Awesome -->
<UButton icon="i-fa-brands-twitter">Twitter</UButton>
</div>
</template>
Browse icons at: https://icones.js.org
Best Practices
- Always wrap app with UApp (v4) - Required for proper functioning
- Use v-model:open for modals (v4) - Breaking change from v3
- Configure colors in app.config.ts (v4) - Centralized theming
- Check package.json version - Use version-appropriate syntax
- Leverage auto-completion - TypeScript types are fully supported
- Use built-in validation - Integrate with Zod for form validation
- Prefer composition - Use slots and composables over prop overload
- Follow Tailwind patterns - Nuxt UI components accept Tailwind classes
- Test dark mode - Always verify components in both light and dark modes
Troubleshooting
Common Issues (v4)
Missing UApp wrapper:
Error: Nuxt UI components require <UApp> wrapper
Solution: Wrap your app in app.vue:
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
Tailwind not working:
Ensure main.css has:
@import "tailwindcss";
v-model not working on Modal:
Use v-model:open instead of v-model in v4.
Migration Checklist
When upgrading from v3 to v4:
- Upgrade to Nuxt 4
- Install Tailwind CSS v4
- Update
main.cssto use@import "tailwindcss" - Wrap app with
<UApp>inapp.vue - Move color config to
app.config.ts - Update
v-modeltov-model:openon Modal/Popover/Dialog - Rename
UButtonGroup→UFieldGroup - Rename
UFormGroup→UFieldGroup - Rename
UVerticalNavigation→UNavigationTree - Test all components in both light and dark mode
- Review official migration guide for additional breaking changes
Official Resources
- Documentation: https://ui.nuxt.com
- Migration Guide: https://ui.nuxt.com/getting-started/migration
- Component Reference: https://ui.nuxt.com/components
- GitHub: https://github.com/nuxt/ui
- Iconify Icons: https://icones.js.org
Note: Always verify version-specific syntax with official documentation at https://ui.nuxt.com. This reference covers v3 and v4, but breaking changes may occur in future releases.