
Nuxt Ui V4
- 329 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
nuxt-ui-v4 is a secondsky Claude skill for Nuxt UI v4.6+ that guides 125+ accessible Vue components, Tailwind v4 theming, and AI SDK chat integration for developers building Nuxt v4 dashboards and SaaS UIs.
About
nuxt-ui-v4 is a secondsky Claude plugin skill for Nuxt UI v4.6+ on Nuxt v4.0.0, bundling 125+ accessible components built on Tailwind CSS v4 and Reka UI. It covers dashboard shells with UDashboardGroup, AI chat with UChatMessages and UChatReasoning tied to AI SDK v5, TipTap UEditor blocks, page-layout heroes, UPricingPlans, and 22 form components with UForm validation. Commands include /nuxt-ui-v4:setup, /nuxt-ui:migrate, /nuxt-ui:theme, and /nuxt-ui:component, plus agents for component selection, v2/v3 migration, and troubleshooting. The official Nuxt UI MCP server at ui.nuxt.com/mcp supplies live component metadata. Developers reach for it when scaffolding Nuxt SaaS dashboards, AI chat UIs, landing pages, or fixing missing UApp wrappers, CSS import order, and AI SDK Chat class usage errors documented across 25+ common failure modes.
- nuxt-ui-v4
Nuxt Ui V4 by the numbers
- 329 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,259 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill nuxt-ui-v4Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 329 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
How do you build dashboards with Nuxt UI v4?
Use nuxt-ui-v4 for development tasks
Who is it for?
Vue and Nuxt v4 developers building dashboards, AI chat interfaces, pricing pages, or admin panels with the Nuxt UI component library and Tailwind v4.
Skip if: React or Next.js projects, Nuxt 3 codebases not ready to migrate, or teams requiring Tailwind CSS v3 without upgrading to v4.
When should I use this skill?
A Nuxt v4 app needs Nuxt UI components, theming, AI chat widgets, migration from v2/v3, or fixes for UApp, CSS import, or AI SDK errors.
What you get
Nuxt v4 project configuration, Vue components using Nuxt UI primitives, theme CSS, and resolved component or TypeScript error fixes.
- nuxt.config.ts module setup
- Vue component scaffolds
- Theme CSS configuration
By the numbers
- Covers 125+ Nuxt UI components for Nuxt UI v4.6+ on Nuxt v4.0.0
- Lists 8 official nuxt create templates including ui/chat and ui/dashboard
- Documents 25+ common Nuxt UI error solutions in COMMON_ERRORS_DETAILED.md
Files
Nuxt UI v4 - Production Component Library
Version: Nuxt UI v4.6+ | Nuxt v4.0.0 | 125+ Components Last Verified: 2026-03-30
A comprehensive production-ready component library with 125+ accessible components, Tailwind CSS v4, Reka UI accessibility, and first-class AI integration. Works with Nuxt and plain Vue apps (Vite, Inertia, SSR).
MCP Integration: This plugin includes the official Nuxt UI MCP server for live component data.
---
When to Use / NOT Use
Use when: Building Nuxt v4 dashboards, AI chat interfaces, landing pages, forms, admin panels, pricing pages, blogs, documentation sites, or any UI with Nuxt UI components
DON'T use: React projects, Nuxt 3 or earlier, Tailwind CSS v3. Vue-only projects ARE supported via Vite plugin.
---
Quick Start
bunx nuxi init my-app && cd my-app
bun add @nuxt/ui tailwindcss// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})/* assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";<!-- app.vue -->
<template><UApp><NuxtPage /></UApp></template>Or use a template:
npm create nuxt@latest -- -t ui # Starter
npm create nuxt@latest -- -t ui/dashboard # Dashboard
npm create nuxt@latest -- -t ui/chat # AI Chat
npm create nuxt@latest -- -t ui/landing # Landing page
npm create nuxt@latest -- -t ui/saas # SaaS
npm create nuxt@latest -- -t ui/docs # Documentation
npm create nuxt@latest -- -t ui/portfolio # Portfolio
npm create nuxt@latest -- -t ui/changelog # Changelog
npm create nuxt@latest -- -t ui/editor # Rich text editorCommands available: /nuxt-ui-v4:setup, /nuxt-ui:migrate, /nuxt-ui:theme, /nuxt-ui:component
Secure Installation
UI packages modify CSS and component trees — verify before allowing into your project. Follow supply chain security best practices:
- Block post-install scripts —
npm config set ignore-scripts true(or Bun: disabled by default) - Cooldown period — Wait 7 days for new package versions to be vetted by the community
- Audit before installing — Run
socket package score npm <pkg>or usesocket npm install <pkg>to check packages
Load the dependency-upgrade skill for full security configuration including Socket CLI integration, cooldown setup, lockfile validation, and CI enforcement.
---
Component Categories (125+ Total)
Dashboard (10 components) - NEW
Complete admin interface system:
- DashboardGroup - Fixed layout wrapper with sidebar state management
- DashboardSidebar - Resizable, collapsible sidebar
- DashboardPanel - Main content panel with header/body/footer slots
- DashboardNavbar - Top navigation bar
- DashboardToolbar - Secondary toolbar under navbar
- DashboardSearch - CommandPalette for dashboard search
- DashboardSearchButton - Button to trigger search
- DashboardSidebarCollapse - Collapse button for desktop
- DashboardSidebarToggle - Toggle button for mobile
- DashboardResizeHandle - Resize handle for sidebar/panels
<template>
<UDashboardGroup>
<UDashboardSidebar>
<UNavigationMenu :items="menuItems" />
</UDashboardSidebar>
<UDashboardPanel>
<template #header><UDashboardNavbar /></template>
<template #body><NuxtPage /></template>
</UDashboardPanel>
</UDashboardGroup>
</template>Details: Load references/dashboard-components.md for complete dashboard patterns
---
Chat / AI (8 components)
Purpose-built for AI chatbots with AI SDK v5:
- ChatMessage - Single message with icon, avatar, actions
- ChatMessages - Message list with auto-scroll, status indicator
- ChatPalette - Chat interface inside an overlay
- ChatPrompt - Enhanced Textarea for AI prompts
- ChatPromptSubmit - Submit button with status handling
- ChatReasoning - Collapsible AI reasoning/thinking process (NEW v4.6)
- ChatTool - Collapsible AI tool invocation status (NEW v4.6)
- ChatShimmer - Text shimmer animation for streaming states (NEW v4.6)
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
const input = ref('')
const chat = new Chat({
onError(error) { console.error(error) }
})
function onSubmit() {
chat.sendMessage({ text: input.value })
input.value = ''
}
</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="isReasoningStreaming(message, index, chat)" />
<UChatTool v-else-if="isToolUIPart(part)" :text="getToolName(part)" :streaming="isToolStreaming(part)" />
<template v-else-if="isTextUIPart(part)">
<MDC v-if="message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" />
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
</template>
</template>
</template>
</UChatMessages>
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="chat.status" @stop="chat.stop()" @reload="chat.regenerate()" />
</UChatPrompt>
</template>Details: Load references/chat-components.md for full AI SDK integration, streaming, reasoning, tool calling
---
Editor (6 components) - NEW
Rich text editing with TipTap:
- Editor - TipTap-based editor with markdown/HTML/JSON support
- EditorToolbar - Fixed, bubble, or floating toolbar
- EditorDragHandle - Drag handle for reordering blocks
- EditorMentionMenu - @ mention suggestions
- EditorEmojiMenu - : emoji picker
- EditorSuggestionMenu - / command menu
<template>
<UEditor v-model="content" :extensions="extensions">
<template #toolbar>
<UEditorToolbar />
</template>
</UEditor>
</template>Details: Load references/editor-components.md for TipTap setup, extensions, toolbar customization
---
Page Layout (16 components) - NEW
Landing pages and content layouts:
- Page - Grid layout with left/right columns
- PageHeader - Responsive page header
- PageHero - Hero section with title, description, CTAs
- PageSection - Content section container
- PageGrid - Responsive grid system
- PageColumns - Multi-column layout
- PageFeature - Feature showcase component
- PageCTA - Call-to-action section
- PageCard - Pre-styled card with title, description, link
- PageList - Vertical list layout
- PageLogos - Logo showcase
- PageAnchors - Anchor link list
- PageAside - Sticky sidebar
- PageBody - Main content area
- PageLinks - Link list
<template>
<UPage>
<UPageHero title="Welcome" description="Get started today" :links="heroLinks" />
<UPageSection>
<UPageGrid>
<UPageFeature v-for="f in features" v-bind="f" />
</UPageGrid>
</UPageSection>
<UPageCTA title="Ready?" :links="ctaLinks" />
</UPage>
</template>Details: Load references/page-layout-components.md for landing page patterns
---
Content (7 components) - NEW
Documentation and blog content:
- BlogPost - Article display component
- BlogPosts - Blog grid layout
- ChangelogVersion - Version entry display
- ChangelogVersions - Changelog timeline
- ContentNavigation - Accordion-style nav for docs
- ContentSearch - Documentation search CommandPalette
- ContentSearchButton - Button to open search
- ContentSurround - Prev/next navigation
- ContentToc - Sticky table of contents
<template>
<UBlogPosts>
<UBlogPost v-for="post in posts" v-bind="post" />
</UBlogPosts>
</template>Details: Load references/content-components.md for blog and documentation patterns
---
Pricing (3 components) - NEW
SaaS pricing pages:
- PricingPlan - Individual plan card
- PricingPlans - Responsive plan grid
- PricingTable - Feature comparison table
<template>
<UPricingPlans>
<UPricingPlan
v-for="plan in plans"
:title="plan.title"
:price="plan.price"
:features="plan.features"
/>
</UPricingPlans>
</template>Details: Load references/pricing-components.md for pricing page patterns
---
Forms (22 components)
Input, InputDate, InputTime, InputNumber, InputTags, InputMenu, Select, SelectMenu, Textarea, Checkbox, CheckboxGroup, RadioGroup, Switch, Slider, Calendar, ColorPicker, PinInput, Form, FormField, FileUpload, FieldGroup, AuthForm
<UForm :state="state" :schema="schema" @submit="onSubmit">
<UFormField name="email" label="Email">
<UInput v-model="state.email" type="email" />
</UFormField>
<UButton type="submit">Submit</UButton>
</UForm>Details: Load references/form-components-reference.md for validation, nested forms, file uploads
---
Navigation (8 components)
Tabs, Breadcrumb, Link, Pagination, CommandPalette, NavigationMenu, Stepper, Tree
<UTabs v-model="tab" :items="items" />
<UCommandPalette :groups="groups" placeholder="Search..." />
<UStepper v-model="step" :items="steps" />Details: Load references/navigation-components-reference.md for patterns
---
Overlays (8 components)
Modal, Drawer, Slideover, Dialog, Popover, DropdownMenu, ContextMenu, Tooltip
<UModal v-model="isOpen"><UCard>Content</UCard></UModal>
<UDrawer v-model="isOpen" side="right">...</UDrawer>Details: Load references/overlay-decision-guide.md for when to use each
---
Feedback (7 components)
Alert, Toast, Progress, Skeleton, Empty, Error, Banner
<UAlert color="warning" title="Warning message" />
<UEmpty icon="i-heroicons-inbox" title="No items" />
<UBanner title="Important announcement" />Details: Load references/feedback-components-reference.md
---
Layout (6 components)
Card, Container, Main, Header, Footer, FooterColumns, Separator
---
Data (2 components)
Table (with virtualization), ScrollArea
---
General (15 components)
Button, FieldGroup, Avatar, AvatarGroup, Badge, Accordion, Carousel, Chip, Collapsible, Icon, Kbd, Marquee, Timeline, User, App
---
Color Mode (6 components)
ColorModeAvatar, ColorModeButton, ColorModeImage, ColorModeSelect, ColorModeSwitch, LocaleSelect
---
Composables
Core: useToast, useOverlay, useFormField, useScrollShadow Utilities: defineShortcuts, defineLocale, extendLocale, extractShortcuts
const { add } = useToast()
add({ title: 'Success', color: 'success' })
defineShortcuts({ 'meta_k': () => openSearch() })AI Utilities: isReasoningStreaming, isToolStreaming, getTextFromMessage (from @nuxt/ui/utils/ai)
---
Common Errors (Top 5)
1. Missing UApp Wrapper → Wrap app with <UApp> 2. CSS Import Order → @import "tailwindcss" FIRST, then @import "@nuxt/ui" 3. Missing tailwindcss package → bun add @nuxt/ui tailwindcss (both required) 4. Module Not Found → Add '@nuxt/ui' to modules in nuxt.config.ts 5. useChat not found → AI SDK v5 uses new Chat() class, not useChat() composable
Full list: Load references/COMMON_ERRORS_DETAILED.md for 25+ error solutions
---
When to Load References
Dashboard/Admin: dashboard-components.md AI Chat: chat-components.md, ai-sdk-v5-integration.md Chat Sub-components: chat-reasoning.md, chat-tool.md, chat-shimmer.md Rich Text: editor-components.md Landing Pages: page-layout-components.md Pricing/SaaS: pricing-components.md Blog/Docs: content-components.md Auth/Login: auth-form.md Forms: form-components-reference.md, form-validation-patterns.md Theming: semantic-color-system.md, component-theming-guide.md Troubleshooting: COMMON_ERRORS_DETAILED.md
---
Available Commands
/nuxt-ui-v4:setup- Initialize Nuxt UI in project/nuxt-ui:migrate- Migrate from v2/v3 to v4/nuxt-ui:theme- Generate theme configuration/nuxt-ui:component- Scaffold component with Nuxt UI patterns
Available Agents
- nuxt-ui-component-selector - Recommends best components for use cases
- nuxt-ui-migration-assistant - Guides v2/v3 → v4 migration
- nuxt-ui-troubleshooter - Diagnoses and fixes common issues
MCP Integration
This plugin includes the official Nuxt UI MCP server (https://ui.nuxt.com/mcp) providing:
- Component listing and metadata
- Documentation access
- Migration guides
- Template discovery
Accessibility Patterns
Reka UI Foundation
Nuxt UI v4 is built on Reka UI, providing:
- WAI-ARIA Compliance - All components follow ARIA authoring practices
- Keyboard Navigation - Full keyboard support
- Focus Management - Proper focus trapping and restoration
- Screen Reader Support - Semantic HTML and ARIA labels
Keyboard Shortcuts
defineShortcuts({
'meta_k': () => openCommandPalette(),
'escape': () => closeModal()
})Focus Management
Components like Modal automatically manage focus:
- Focus moves to first focusable element
- Tab cycles through elements
- Escape closes and returns focus
Best Practices
1. Always provide alt text for images 2. Use semantic HTML 3. Test with keyboard only 4. Test with screen readers 5. Ensure sufficient color contrast
AI SDK v5 Integration
Complete guide for integrating Vercel AI SDK v5 with Nuxt UI v4 Chat components.
---
Installation
bun add ai @ai-sdk/vue @ai-sdk/gatewayAdditional providers (optional, use instead of gateway):
bun add @ai-sdk/openai # OpenAI
bun add @ai-sdk/anthropic # Anthropic
bun add @ai-sdk/google # Google---
Server Setup
Basic Endpoint
// server/api/chat.post.ts
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
}).toUIMessageStreamResponse()
})With Reasoning Support
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
providerOptions: {
anthropic: {
thinking: { type: 'adaptive' },
effort: 'low'
},
google: {
thinkingConfig: { includeThoughts: true, thinkingLevel: 'low' }
},
openai: {
reasoningEffort: 'low',
reasoningSummary: 'detailed'
}
}
}).toUIMessageStreamResponse()
})With Web Search
import { anthropic } from '@ai-sdk/anthropic'
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
messages: await convertToModelMessages(messages),
tools: {
web_search: anthropic.tools.webSearch_20250305({})
}
}).toUIMessageStreamResponse()With MCP Tool Calling
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { experimental_createMCPClient } from '@ai-sdk/mcp'
import { streamText, convertToModelMessages, stepCountIs } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const httpClient = await experimental_createMCPClient({
transport: new StreamableHTTPClientTransport(new URL('https://your-app.com/mcp'))
})
const tools = await httpClient.tools()
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(6),
tools,
onFinish: async () => { await httpClient.close() },
onError: async (error) => { console.error(error); await httpClient.close() }
}).toUIMessageStreamResponse()
})---
Client Setup
Chat Class (replaces useChat)
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'
const chat = new Chat({
onError(error) {
console.error(error)
}
})
</script>Chat Methods
chat.sendMessage({ text: 'Hello' }) // Send message
chat.stop() // Stop streaming
chat.regenerate() // Regenerate last response
chat.setMessages([]) // Replace messages
chat.messages // UIMessage[] - reactive
chat.status // 'submitted' | 'streaming' | 'ready' | 'error'
chat.error // Error | undefined---
Parts-Based Rendering
AI SDK v5 uses message.parts instead of message.content. Each part has a type field.
Import Helpers
// From 'ai' package
import {
isReasoningUIPart, // Check if part is reasoning content
isTextUIPart, // Check if part is text content
isToolUIPart, // Check if part is tool invocation
getToolName // Extract tool name from tool part
} from 'ai'
// From '@nuxt/ui/utils/ai'
import {
isReasoningStreaming, // Check if reasoning part is currently streaming
isToolStreaming, // Check if tool part is still running
getTextFromMessage // Extract all text from message parts
} from '@nuxt/ui/utils/ai'Full Rendering Pattern
<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="isReasoningStreaming(message, index, chat)"
>
<MDC :value="part.text" :cache-key="`reasoning-${message.id}-${index}`" />
</UChatReasoning>
<UChatTool
v-else-if="isToolUIPart(part)"
:text="getToolName(part)"
:streaming="isToolStreaming(part)"
/>
<template v-else-if="isTextUIPart(part)">
<MDC
v-if="message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
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>
<UChatPrompt v-model="input" :error="chat.error" @submit="onSubmit">
<UChatPromptSubmit
:status="chat.status"
@stop="chat.stop()"
@reload="chat.regenerate()"
/>
</UChatPrompt>
</template>---
Migration from AI SDK v4
useChat → Chat Class
- import { useChat } from '@ai-sdk/vue'
+ import { Chat } from '@ai-sdk/vue'
- const { messages, input, handleSubmit, status, error, reload, setMessages } = useChat()
+ const input = ref('')
+ const chat = new Chat({ onError(error) { console.error(error) } })content → parts
- setMessages([{ id: '1', role: 'user', content: 'Hello' }])
+ // Messages now use parts instead of content
+ // Handled automatically by Chat classMethod Renames
- reload()
+ chat.regenerate()
- :messages="messages"
- :status="status"
+ :messages="chat.messages"
+ :status="chat.status"---
Official Templates
Nuxt UI provides production-ready chat templates:
- Nuxt: https://github.com/nuxt-ui-templates/chat
- Vue: https://github.com/nuxt-ui-templates/chat-vue
npm create nuxt@latest -- -t ui/chatAuthForm Component
A customizable Form to create login, register or password reset forms. Built on top of the Form component with field generation and provider support.
---
Props
interface AuthFormProps {
as?: any // Element or component to render as
icon?: any // Icon above the title
title?: string // Form title
description?: string // Form description
fields?: AuthFormField[] // Auto-generated form fields
providers?: ButtonProps[] // OAuth provider buttons
separator?: string | SeparatorProps // Separator between providers and fields (default: "or")
submit?: Omit<ButtonProps, LinkPropsKeys> // Submit button config (default: { label: 'Continue', block: true })
schema?: T // Zod/Standard Schema for validation
validate?: Function // Custom validation function
validateOn?: FormInputEvents[] // When to validate
disabled?: boolean // Disable entire form
loading?: boolean // Show loading state
loadingAuto?: boolean // Auto-detect loading from submit
ui?: { // Theme customization
root?, header?, leading?, leadingIcon?, title?, description?,
body?, providers?, checkbox?, select?, password?, otp?,
input?, separator?, form?, footer?
}
// ...plus all native <form> HTML attributes
}Field Definition
interface AuthFormField {
name: string
type: 'checkbox' | 'select' | 'otp' | InputHTMLAttributes['type']
label?: string
placeholder?: string
required?: boolean
// Checkbox fields: accepts Checkbox props
// Select fields: accepts SelectMenu props
// OTP fields: accepts PinInput props
// All other types: accepts Input props
// Plus any FormField props
}Slots
header- Custom header contentleading- Before titletitle- Custom titledescription- Custom descriptionproviders- Custom provider buttonsvalidation- Validation error displaysubmit- Custom submit buttonfooter- Footer content
Events
@submit- Form submission with typed data (FormSubmitEvent)
Expose
Access via useTemplateRef:
formRef- Reference to HTML form elementstate- Reactive form state
Usage
Basic Login Form
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'
const fields: AuthFormField[] = [
{ name: 'email', type: 'email', label: 'Email', placeholder: 'Enter your email', required: true },
{ name: 'password', label: 'Password', type: 'password', placeholder: 'Enter your password', required: true },
{ name: 'remember', label: 'Remember me', type: 'checkbox' }
]
const providers = [
{ label: 'Google', icon: 'i-simple-icons-google', onClick: () => handleGoogleLogin() },
{ label: 'GitHub', icon: 'i-simple-icons-github', onClick: () => handleGitHubLogin() }
]
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
function onSubmit(payload: FormSubmitEvent<z.output<typeof schema>>) {
console.log('Submitted', payload)
}
</script>
<template>
<UAuthForm
:schema="schema"
title="Login"
description="Enter your credentials to access your account."
icon="i-lucide-user"
:fields="fields"
:providers="providers"
@submit="onSubmit"
/>
</template>In a PageCard
<template>
<div class="flex flex-col items-center justify-center gap-4 p-4">
<UPageCard class="w-full max-w-md">
<UAuthForm
:schema="schema"
:fields="fields"
:providers="providers"
title="Welcome back!"
icon="i-lucide-lock"
@submit="onSubmit"
>
<template #description>
Don't have an account? <ULink to="#" class="text-primary font-medium">Sign up</ULink>.
</template>
<template #password-hint>
<ULink to="#" class="text-primary font-medium" tabindex="-1">Forgot password?</ULink>
</template>
<template #validation>
<UAlert color="error" icon="i-lucide-info" title="Error signing in" />
</template>
<template #footer>
By signing in, you agree to our <ULink to="#" class="text-primary font-medium">Terms of Service</ULink>.
</template>
</UAuthForm>
</UPageCard>
</div>
</template>Register Form
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
const fields: AuthFormField[] = [
{ name: 'name', type: 'text', label: 'Full Name', placeholder: 'Enter your name', required: true },
{ name: 'email', type: 'email', label: 'Email', placeholder: 'Enter your email', required: true },
{ name: 'password', type: 'password', label: 'Password', placeholder: 'Create a password', required: true },
{ name: 'confirmPassword', type: 'password', label: 'Confirm Password', placeholder: 'Confirm your password', required: true }
]
</script>
<template>
<UAuthForm
title="Create account"
description="Fill in the details below to get started."
icon="i-lucide-user-plus"
:fields="fields"
:submit="{ label: 'Create Account', block: true }"
separator="Providers"
@submit="onRegister"
/>
</template>OTP Verification
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
const fields: AuthFormField[] = [
{ name: 'otp', type: 'otp', label: 'Verification Code' }
]
</script>
<template>
<UAuthForm
title="Verify your email"
description="Enter the 6-digit code sent to your email."
icon="i-lucide-shield-check"
:fields="fields"
:submit="{ label: 'Verify', block: true }"
@submit="onVerify"
/>
</template>Theme
export default defineAppConfig({
ui: {
authForm: {
slots: {
root: 'w-full space-y-6',
header: 'flex flex-col text-center',
leading: 'mb-2',
leadingIcon: 'size-8 shrink-0 inline-block',
title: 'text-xl text-pretty font-semibold text-highlighted',
description: 'mt-1 text-base text-pretty text-muted',
body: 'gap-y-6 flex flex-col',
providers: 'space-y-3',
form: 'space-y-5',
footer: 'text-sm text-center text-muted mt-2'
}
}
}
})Chat Components Reference
Nuxt UI v4 provides 8 purpose-built components for AI chatbots, designed to work with Vercel AI SDK v5.
Component Overview
| Component | Purpose |
|---|---|
| ChatMessages | Scrollable message list with auto-scroll and loading indicator |
| ChatMessage | Individual message bubble with avatar, actions, and slots |
| ChatPrompt | Enhanced textarea for submitting prompts |
| ChatPromptSubmit | Submit button with automatic status handling |
| ChatReasoning | Collapsible AI reasoning/thinking process |
| ChatTool | Collapsible AI tool invocation status |
| ChatShimmer | Text shimmer animation for streaming states |
| ChatPalette | Layout wrapper for embedding chat in overlays |
Quick Start
Install Dependencies
bun add ai @ai-sdk/vue @ai-sdk/gatewayCreate API Endpoint
// server/api/chat.post.ts
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('anthropic/claude-sonnet-4-6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
}).toUIMessageStreamResponse()
})Reasoning Support
// server/api/chat.post.ts - with reasoning
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
providerOptions: {
anthropic: {
thinking: { type: 'adaptive' },
effort: 'low'
}
}
}).toUIMessageStreamResponse()MCP Tool Calling
// server/api/chat.post.ts - with MCP tools
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { experimental_createMCPClient } from '@ai-sdk/mcp'
import { stepCountIs } from 'ai'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const httpClient = await experimental_createMCPClient({
transport: new StreamableHTTPClientTransport(new URL('https://your-app.com/mcp'))
})
const tools = await httpClient.tools()
return streamText({
model: gateway('anthropic/claude-sonnet-4-6'),
maxOutputTokens: 10000,
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(6),
tools,
onFinish: async () => { await httpClient.close() },
onError: async (error) => { console.error(error); await httpClient.close() }
}).toUIMessageStreamResponse()
})Create Chat Interface
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
const input = ref('')
const chat = new Chat({
onError(error) {
console.error(error)
}
})
function onSubmit() {
chat.sendMessage({ text: input.value })
input.value = ''
}
</script>
<template>
<div class="flex flex-col h-full">
<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="isReasoningStreaming(message, index, chat)"
>
<MDC
:value="part.text"
:cache-key="`reasoning-${message.id}-${index}`"
class="*:first:mt-0 *:last:mb-0"
/>
</UChatReasoning>
<UChatTool
v-else-if="isToolUIPart(part)"
:text="getToolName(part)"
:streaming="isToolStreaming(part)"
/>
<template v-else-if="isTextUIPart(part)">
<MDC
v-if="message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
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>
<UChatPrompt v-model="input" :error="chat.error" @submit="onSubmit">
<UChatPromptSubmit
:status="chat.status"
@stop="chat.stop()"
@reload="chat.regenerate()"
/>
</UChatPrompt>
</div>
</template>ChatMessages
Displays a list of messages with auto-scroll and status indicator.
Props
interface ChatMessagesProps {
messages?: UIMessage[] // Message array from AI SDK
status?: ChatStatus // 'submitted' | 'streaming' | 'ready' | 'error'
shouldAutoScroll?: boolean // Auto-scroll on stream (default: false)
shouldScrollToBottom?: boolean // Scroll on mount (default: true)
autoScroll?: boolean | ButtonProps // Auto-scroll button config
autoScrollIcon?: string // Icon for scroll button
user?: Partial<ChatMessageProps> // User message defaults
assistant?: Partial<ChatMessageProps> // Assistant message defaults
compact?: boolean // Compact mode
spacingOffset?: number // Offset for sticky prompt
}Status Values
submitted- Message sent, awaiting responsestreaming- Response actively streamingready- Ready for new messageerror- Error occurred
Slots
default- Custom message renderingleading- Before message contentcontent- Message contentactions- Message actionsindicator- Loading indicatorviewport- Auto-scroll area
Usage
<UChatMessages
:messages="chat.messages"
:status="chat.status"
:should-auto-scroll="true"
:user="{ variant: 'soft', side: 'right' }"
:assistant="{ variant: 'naked', side: 'left', avatar: { src: '/ai.png' } }"
>
<template #content="{ message }">
<MDC :value="getTextFromMessage(message)" />
</template>
<template #indicator>
<UButton loading label="Thinking..." variant="link" />
</template>
</UChatMessages>ChatMessage
Single message display with avatar, icon, and actions.
Props
interface ChatMessageProps {
side?: 'left' | 'right' // Message alignment
variant?: 'solid' | 'soft' | 'subtle' | 'naked'
icon?: string // Message icon
avatar?: AvatarProps // Avatar props
actions?: DropdownMenuItem[] // Action menu items
}Slots
leading- Before content (avatar/icon)default- Message contenttrailing- After content (actions)
Usage
<UChatMessage
side="left"
variant="naked"
:avatar="{ src: '/ai-avatar.png' }"
:actions="[
{ label: 'Copy', icon: 'i-heroicons-clipboard' },
{ label: 'Regenerate', icon: 'i-heroicons-arrow-path' }
]"
>
Hello! How can I help you today?
</UChatMessage>ChatPrompt
Enhanced textarea for chat input with submit handling.
Props
interface ChatPromptProps {
modelValue?: string // v-model binding
placeholder?: string // Placeholder text
disabled?: boolean // Disable input
loading?: boolean // Show loading state
autofocus?: boolean // Focus on mount
autoresize?: boolean // Auto-resize height
maxRows?: number // Max rows when autoresizing
}Events
@update:modelValue- Input change@submit- Form submission (Enter or button)
Slots
leading- Before textareadefault- Submit button areatrailing- After textarea
Usage
<UChatPrompt
v-model="input"
placeholder="Type a message..."
:disabled="chat.status === 'streaming'"
@submit="sendMessage"
>
<template #leading>
<UButton icon="i-heroicons-paper-clip" variant="ghost" />
</template>
<UChatPromptSubmit :status="chat.status" />
</UChatPrompt>ChatPromptSubmit
Submit button with automatic status handling.
Props
interface ChatPromptSubmitProps {
status?: ChatStatus // Current chat status
submitIcon?: string // Submit icon
stopIcon?: string // Stop icon
reloadIcon?: string // Reload icon
}Events
@stop- Stop streaming@reload- Regenerate response
Usage
<UChatPromptSubmit
:status="chat.status"
submit-icon="i-heroicons-paper-airplane"
stop-icon="i-heroicons-stop"
@stop="chat.stop()"
@reload="chat.regenerate()"
/>ChatPalette
Chat interface inside an overlay (modal/slideover).
Usage
<script setup>
const isOpen = ref(false)
</script>
<template>
<UButton @click="isOpen = true">Open Chat</UButton>
<UChatPalette v-model="isOpen">
<!-- ChatPalette wraps ChatMessages + ChatPrompt -->
</UChatPalette>
</template>ChatReasoning
Collapsible AI reasoning/thinking process. Auto-opens during streaming, auto-closes after.
<template>
<UChatReasoning
:text="part.text"
:streaming="isReasoningStreaming(message, index, chat)"
icon="i-lucide-brain"
/>
</template>Load `references/chat-reasoning.md` for full props, slots, and theme.
ChatTool
Collapsible AI tool invocation status with inline or card variant.
<template>
<UChatTool
:text="getToolName(part)"
:streaming="isToolStreaming(part)"
variant="card"
icon="i-lucide-terminal"
>
<pre v-text="toolOutput" />
</UChatTool>
</template>Load `references/chat-tool.md` for full props, variants, and theme.
ChatShimmer
Text shimmer animation for streaming states. Automatically used by ChatReasoning and ChatTool.
<template>
<UChatShimmer text="Thinking..." :duration="2" :spread="2" />
</template>Load `references/chat-shimmer.md` for full props.
AI SDK v5 Integration
Chat Class Setup
import { Chat } from '@ai-sdk/vue'
const chat = new Chat({
onError(error) {
console.error(error)
}
})Available Methods
chat.sendMessage({ text: 'Hello' }) // Send message
chat.stop() // Stop streaming
chat.regenerate() // Regenerate last response
chat.setMessages([]) // Clear messages
chat.messages // UIMessage[] array
chat.status // 'submitted' | 'streaming' | 'ready' | 'error'
chat.error // Error if anyParts-Based Rendering
AI SDK v5 uses message parts for rich content. Import helpers from ai and @nuxt/ui/utils/ai:
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<UChatReasoning v-if="isReasoningUIPart(part)" :text="part.text" :streaming="isReasoningStreaming(message, index, chat)" />
<UChatTool v-else-if="isToolUIPart(part)" :text="getToolName(part)" :streaming="isToolStreaming(part)" />
<template v-else-if="isTextUIPart(part)">
<MDC v-if="message.role === 'assistant'" :value="part.text" />
<p v-else class="whitespace-pre-wrap">{{ part.text }}</p>
</template>
</template>Load `references/ai-sdk-v5-integration.md` for complete integration guide.
Styling
Theming
export default defineAppConfig({
ui: {
chatMessages: {
slots: {
root: 'flex flex-col gap-1 px-2.5',
indicator: 'h-6 flex items-center gap-1',
autoScroll: 'rounded-full absolute bottom-0 right-1/2'
}
},
chatMessage: {
slots: {
root: 'flex gap-3',
content: 'flex-1'
},
variants: {
side: {
left: { root: 'flex-row' },
right: { root: 'flex-row-reverse' }
}
}
},
chatPrompt: {
base: 'relative flex items-end gap-2 p-2'
}
}
})Common Patterns
Chat with Parts-Based Rendering (Recommended)
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
</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="isReasoningStreaming(message, index, chat)" />
<UChatTool v-else-if="isToolUIPart(part)" :text="getToolName(part)" :streaming="isToolStreaming(part)" />
<template v-else-if="isTextUIPart(part)">
<MDC v-if="message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" />
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
</template>
</template>
</template>
</UChatMessages>
</template>Chat in Dashboard
<UDashboardPanel>
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status" />
</UContainer>
</template>
<template #footer>
<UContainer class="pb-4">
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="chat.status" />
</UChatPrompt>
</UContainer>
</template>
</UDashboardPanel>ChatReasoning Component
Display a collapsible AI reasoning or thinking process. Auto-opens during streaming and auto-closes after.
---
Props
interface ChatReasoningProps {
text?: string // Reasoning text content
streaming?: boolean // Whether actively streaming (default: false)
duration?: number // Reasoning duration in seconds (auto-calculated if omitted)
icon?: any // Icon next to trigger
chevron?: 'leading' | 'trailing' // Chevron position (default: 'trailing')
chevronIcon?: any // Custom chevron icon
autoCloseDelay?: number // Delay before auto-close after streaming ends (default: 500, 0 = disabled)
shimmer?: Partial<ChatShimmerProps> // Customize ChatShimmer when streaming
open?: boolean // Controlled open state (v-model)
defaultOpen?: boolean // Initial open state
unmountOnHide?: boolean // Unmount when closed (default: false)
disabled?: boolean // Prevent interaction
ui?: { root?, trigger?, leading?, leadingIcon?, chevronIcon?, label?, trailingIcon?, content?, body? }
}Events
@update:open- Open state changes
Slots
default- Custom body content (overrides text prop)
Usage
Basic
<template>
<UChatReasoning text="The user is asking about Vue components..." />
</template>With Streaming Detection
<script setup lang="ts">
import { isReasoningStreaming } from '@nuxt/ui/utils/ai'
import { isReasoningUIPart } from 'ai'
// Inside ChatMessages #content slot
</script>
<template>
<UChatReasoning
v-if="isReasoningUIPart(part)"
:text="part.text"
:streaming="isReasoningStreaming(message, index, chat)"
>
<MDC
:value="part.text"
:cache-key="`reasoning-${message.id}-${index}`"
class="*:first:mt-0 *:last:mb-0"
/>
</UChatReasoning>
</template>With Custom Icon
<template>
<UChatReasoning
icon="i-lucide-brain"
chevron="leading"
text="Analyzing your request..."
/>
</template>Theme
export default defineAppConfig({
ui: {
chatReasoning: {
slots: {
root: '',
trigger: 'group flex w-full items-center gap-1.5 text-muted text-sm',
leading: 'relative size-4 shrink-0',
leadingIcon: 'size-4 shrink-0',
chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
label: 'truncate',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
body: 'max-h-[200px] pt-2 overflow-y-auto text-sm text-dimmed whitespace-pre-wrap'
}
}
}
})Notes
- Body content uses
useScrollShadowcomposable for fade shadows when overflowing - When
chevronisleadingwithicon, the icon swaps with chevron on hover/open - When streaming ends, auto-closes after
autoCloseDelayms (default 500)
ChatShimmer Component
Text shimmer animation effect for streaming/loading states in chat interfaces.
---
Props
interface ChatShimmerProps {
text: string // Text to display with shimmer effect (required)
as?: any // Element or component to render as (default: 'span')
duration?: number // Animation duration in seconds (default: 2)
spread?: number // Shimmer highlight width multiplier (default: 2)
// Actual spread = text.length * spread in pixels
}Usage
Basic
<template>
<UChatShimmer text="Thinking..." />
</template>Custom Duration
<template>
<UChatShimmer text="Searching..." :duration="4" />
</template>Custom Spread
<template>
<UChatShimmer text="Loading..." :spread="5" />
</template>Theme
export default defineAppConfig({
ui: {
chatShimmer: {
base: 'text-transparent bg-clip-text bg-no-repeat bg-size-[calc(200%+var(--spread)*2+2px)_100%,auto] bg-[image:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--ui-text-highlighted),#0000_calc(50%+var(--spread))),linear-gradient(var(--ui-text-muted),var(--ui-text-muted))] animate-[shimmer_var(--duration)_linear_infinite] rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite] will-change-[background-position]'
}
}
})Notes
- Automatically used by
ChatToolandChatReasoningwhen streaming - RTL support included via separate animation
- Uses CSS variables
--durationand--spreadfor runtime customization - No slots or events - purely presentational
ChatTool Component
Display a collapsible AI tool invocation status. Shows tool name, loading state, and optional output.
---
Props
interface ChatToolProps {
text?: string // Tool status text (e.g., "Searching components")
suffix?: string // Secondary text after label (e.g., "Button")
icon?: any // Icon next to trigger
loading?: boolean // Show loading indicator (default: false)
loadingIcon?: any // Custom loading icon (default: i-lucide-loader-circle)
streaming?: boolean // Whether tool is actively running (default: false)
variant?: 'inline' | 'card' // Visual variant (default: 'inline')
chevron?: 'leading' | 'trailing' // Chevron position (default: 'trailing')
chevronIcon?: any // Custom chevron icon
shimmer?: Partial<ChatShimmerProps> // Customize ChatShimmer when streaming
open?: boolean // Controlled open state (v-model)
defaultOpen?: boolean // Initial open state
unmountOnHide?: boolean // Unmount when closed (default: false)
disabled?: boolean // Prevent interaction
ui?: { root?, trigger?, leading?, leadingIcon?, chevronIcon?, label?, suffix?, trailingIcon?, content?, body? }
}Events
@update:open- Open state changes
Slots
default- Tool output content (makes it collapsible)
Usage
Basic
<template>
<UChatTool text="Searched components" />
</template>With Tool Detection
<script setup lang="ts">
import { isToolUIPart, getToolName } from 'ai'
import { isToolStreaming } from '@nuxt/ui/utils/ai'
</script>
<template>
<UChatTool
v-if="isToolUIPart(part)"
:text="getToolName(part)"
:streaming="isToolStreaming(part)"
/>
</template>Card Variant with Output
<template>
<UChatTool
text="Running lint checks"
suffix="cd, pnpm run"
:streaming="streaming"
icon="i-lucide-terminal"
variant="card"
chevron="leading"
>
<pre language="bash" v-text="result" />
</UChatTool>
</template>With Loading State
<template>
<UChatTool loading text="Searching components..." />
</template>Variants
| Variant | Description |
|---|---|
inline | Default inline style, blends with message text |
card | Card style with border, padding, and scrollable output area |
Theme
export default defineAppConfig({
ui: {
chatTool: {
slots: {
root: '',
trigger: 'group flex w-full items-center gap-1.5 text-muted text-sm',
leading: 'relative size-4 shrink-0',
leadingIcon: 'size-4 shrink-0',
label: 'truncate',
suffix: 'text-dimmed ms-1',
body: 'text-sm text-dimmed whitespace-pre-wrap'
},
variants: {
variant: {
inline: { body: 'pt-2' },
card: {
root: 'rounded-md ring ring-default overflow-hidden',
trigger: 'px-2 py-1',
body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
}
},
loading: {
true: { leadingIcon: 'animate-spin' }
}
},
defaultVariants: { variant: 'inline' }
}
}
})Notes
- Without a default slot, renders as a simple inline status (non-collapsible)
- With a default slot, becomes collapsible to show/hide tool output
- Streaming state shows a
ChatShimmeranimation on the label text - Loading state spins the leading icon
CommandPalette Setup
Installation (if using search)
npm install fuse.jsBasic Setup
<script setup lang="ts">
const isOpen = ref(false)
const groups = computed(() => [
{
key: 'actions',
label: 'Actions',
commands: [
{
id: 'new',
label: 'New File',
icon: 'i-heroicons-document-plus',
shortcuts: ['⌘', 'N']
}
]
}
])
defineShortcuts({
'meta_k': () => { isOpen.value = !isOpen.value }
})
</script>
<template>
<UCommandPalette v-model="isOpen" :groups="groups" />
</template>With Search (Fuse.js)
import Fuse from 'fuse.js'
const allCommands = [...]
const fuse = new Fuse(allCommands, {
keys: ['label', 'description'],
threshold: 0.3
})
const searchQuery = ref('')
const filteredCommands = computed(() => {
if (!searchQuery.value) return allCommands
return fuse.search(searchQuery.value).map(r => r.item)
})Async Data
const groups = computed(async () => {
const data = await $fetch('/api/commands')
return data
})Nuxt UI v4 - Common Errors & Detailed Solutions
Complete troubleshooting guide for Nuxt UI v4 with 25 common errors and their solutions.
Last Updated: 2026-03-30
---
Error Index
Jump to specific errors:
- 1. Missing UApp Wrapper
- 2. Module Not Registered
- 3. CSS Import Order
- 4. useToast Not Imported
- 5. Toast Positioning Conflicts
- 6. CommandPalette Shortcuts Not Working
- 7. Carousel Not Displaying
- 8. Drawer Not Responsive
- 9. Modal vs Dialog Confusion
- 10. Popover Positioning Issues
- 11. Skeleton Dimensions Wrong
- 12. Card Slots Not Working
- 13. Avatar Fallback Missing
- 14. Badge Positioning Wrong
- 15. Form Nested Prop Missing
- 16. Table Pagination State
- 17. Color Mode Not Persisting
- 18. TypeScript Types Not Generated
- 19. Theme Variants Not Applying
- 20. Responsive Patterns Broken
- 21. Cannot read property 'focus' of undefined (v4.2+)
- 22. Missing tailwindcss Package
- 23. useChat Not Found (AI SDK v5)
- 24. Chat messages.content Not Working
- 25. Form Nested Validation Not Inherited
---
1. Missing UApp Wrapper
Error: Components not rendering or styles missing
Symptoms:
- Nuxt UI components render without styles
- Dark mode not working
- Components appear as unstyled HTML
- Console errors about missing context
Root Cause: <UApp> provides the required Vue context for all Nuxt UI components, including color mode, toast container, and global styles.
Solution: Wrap your entire app with <UApp> in app.vue:
<!-- app.vue -->
<template>
<UApp>
<NuxtPage />
</UApp>
</template>Why this works: <UApp> initializes:
- Color mode provider (
useColorMode) - Toast notification container
- Global CSS variables
- Component context
---
2. Module Not Registered
Error: "Cannot find module @nuxt/ui" or components not recognized
Symptoms:
- TypeScript errors for component names
- Components don't render
- Module import errors in console
- Auto-imports not working
Root Cause: @nuxt/ui module not registered in Nuxt configuration.
Solution: Add module to nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui']
})Additional troubleshooting:
# Verify installation
bun list | grep @nuxt/ui
# Reinstall if necessary
bun remove @nuxt/ui
bun add @nuxt/ui
# Clear .nuxt cache
rm -rf .nuxt
bunx nuxt prepare---
3. CSS Import Order
Error: Styles not applying correctly, Tailwind classes overridden
Symptoms:
- Component styles look broken
- Tailwind utilities don't work
- Dark mode colors incorrect
- Custom theme not applying
Root Cause: Incorrect CSS import order causes Tailwind to override Nuxt UI styles.
Solution: Import Tailwind CSS before Nuxt UI in your global CSS file:
<!-- app.vue or layouts/default.vue -->
<style>
@import "tailwindcss"; /* First - base Tailwind */
@import "@nuxt/ui"; /* Second - Nuxt UI components */
</style>Why this matters: Nuxt UI's component styles should override Tailwind's base styles to ensure proper theming.
---
4. useToast Not Imported
Error: "useToast is not defined" or "Cannot read property 'add' of undefined"
Symptoms:
- Runtime error when calling toast functions
- TypeScript error in IDE
- Toast notifications don't appear
Root Cause: useToast composable not imported (not auto-imported in some contexts).
Solution: Explicitly import or use auto-import:
// Auto-import (recommended in components)
const { add } = useToast()
add({
title: 'Success',
description: 'Operation completed'
})
// Explicit import (needed in utils/stores)
import { useToast } from '#app'
const toast = useToast()
toast.add({ title: 'Message' })---
5. Toast Positioning Conflicts
Error: Toasts appearing in wrong location or overlapping with other UI
Symptoms:
- Toasts appear in unexpected corner
- Multiple toasts stack incorrectly
- Toasts hidden behind other elements
- Z-index conflicts
Root Cause: Default toast position conflicts with app layout or not configured.
Solution: Configure position in app.config.ts:
// app.config.ts
export default defineAppConfig({
ui: {
toast: {
position: 'top-right', // Options: top-right, top-left, bottom-right, bottom-left, top-center, bottom-center
container: 'fixed z-50 inset-0 pointer-events-none p-4',
wrapper: 'w-full pointer-events-auto'
}
}
})Per-toast positioning:
const { add } = useToast()
add({
title: 'Custom positioned',
position: 'bottom-center' // Override global setting
})---
6. CommandPalette Shortcuts Not Working
Error: Keyboard shortcuts (Cmd+K) not triggering CommandPalette
Symptoms:
- Keyboard shortcuts don't open palette
- Other shortcuts conflict
- Shortcuts work in some components but not others
Root Cause: Shortcuts not registered with defineShortcuts composable.
Solution: Use defineShortcuts composable:
<script setup lang="ts">
const isOpen = ref(false)
defineShortcuts({
'meta_k': {
handler: () => {
isOpen.value = !isOpen.value
}
},
'ctrl_k': { // Alternative for non-Mac
handler: () => {
isOpen.value = !isOpen.value
}
}
})
</script>
<template>
<UCommandPalette v-model="isOpen" :groups="groups" />
</template>Global shortcuts (in app.vue):
defineShortcuts({
'escape': () => closeAllModals(),
'meta_/': () => openSearch()
})---
7. Carousel Not Displaying
Error: Carousel components not working or slides not navigating
Symptoms:
- Carousel renders blank
- Slides don't transition
- Navigation arrows don't work
- Console error about Embla
Root Cause: Missing embla-carousel-vue peer dependency.
Solution: Install Embla Carousel:
bun add embla-carousel-vueUsage:
<template>
<UCarousel :items="slides" />
</template>
<script setup lang="ts">
const slides = [
{ id: 1, content: 'Slide 1' },
{ id: 2, content: 'Slide 2' },
{ id: 3, content: 'Slide 3' }
]
</script>---
8. Drawer Not Responsive
Error: Drawer doesn't adapt to mobile screen sizes
Symptoms:
- Drawer too wide on mobile
- Drawer opens from wrong side
- Backdrop doesn't cover full screen on mobile
Root Cause: No responsive breakpoint handling for overlay components.
Solution: Use responsive patterns with useMediaQuery:
<template>
<!-- Desktop: Modal, Mobile: Drawer -->
<UModal v-if="!isMobile" v-model="isOpen">
<UCard>
<p>Desktop modal content</p>
</UCard>
</UModal>
<UDrawer v-else v-model="isOpen" side="right">
<UCard>
<p>Mobile drawer content</p>
</UCard>
</UDrawer>
</template>
<script setup lang="ts">
const isOpen = ref(false)
const isMobile = useMediaQuery('(max-width: 768px)')
</script>Alternative: Sheet for mobile-first:
<USheet v-model="isOpen">
<!-- Automatically becomes bottom sheet on mobile -->
</USheet>---
9. Modal vs Dialog Confusion
Error: Using wrong overlay component for the use case
Symptoms:
- Overlay doesn't behave as expected
- Too much or too little functionality
- Accessibility issues
Root Cause: Not understanding component purposes.
Decision Guide:
| Component | Use Case | Features |
|---|---|---|
| Modal | Full-featured overlays, forms, content | Backdrop, keyboard nav, header/footer slots |
| Dialog | Confirmation prompts, alerts | Simple yes/no, auto-focus confirm button |
| Drawer | Side panels, filters, navigation | Slides from edge, mobile-friendly |
| Popover | Contextual info, tooltips with interaction | Positioned relative to trigger, auto-dismiss |
| Sheet | Bottom drawers, mobile actions | Bottom slide-up, iOS-style |
| Tooltip | Hover-only info | No interaction, auto-hide on unhover |
Examples:
<!-- ✓ Modal: Complex form -->
<UModal v-model="showEditForm">
<UForm @submit="onSubmit">
<UFormGroup label="Name">
<UInput v-model="name" />
</UFormGroup>
</UForm>
</UModal>
<!-- ✓ Dialog: Confirmation -->
<UDialog
title="Delete item?"
description="This action cannot be undone"
:actions="[
{ label: 'Cancel', color: 'neutral' },
{ label: 'Delete', color: 'red', onClick: deleteItem }
]"
/>
<!-- ✓ Drawer: Filters -->
<UDrawer v-model="showFilters" side="right">
<UCard>
<template #header>Filters</template>
<!-- Filter options -->
</UCard>
</UDrawer>
<!-- ✓ Popover: Contextual actions -->
<UPopover>
<UButton>Options</UButton>
<template #panel>
<div class="p-2">
<UButton variant="ghost">Edit</UButton>
<UButton variant="ghost">Delete</UButton>
</div>
</template>
</UPopover>---
10. Popover Positioning Issues
Error: Popover appears in wrong position or gets cut off
Symptoms:
- Popover off-screen
- Popover covers trigger button
- Incorrect alignment
Root Cause: Default positioning doesn't account for viewport edges or scroll.
Solution: Configure placement and flip behavior:
<UPopover
placement="bottom-start"
:flip="true"
:offset="8"
>
<UButton>Trigger</UButton>
<template #panel>
<div class="p-4">Content</div>
</template>
</UPopover>All placement options:
top,top-start,top-endbottom,bottom-start,bottom-endleft,left-start,left-endright,right-start,right-end
Auto-flip (default true): Automatically flips to opposite side if no space
---
11. Skeleton Dimensions Wrong
Error: Skeleton loader doesn't match actual content dimensions
Symptoms:
- Layout shift when content loads
- Skeleton too small or too large
- Poor loading UX
Root Cause: Skeleton dimensions don't match real content.
Solution: Match exact dimensions of actual elements:
<template>
<!-- Loading state -->
<div v-if="loading">
<USkeleton class="h-12 w-64 mb-4" /> <!-- Matches heading -->
<USkeleton class="h-4 w-full mb-2" /> <!-- Matches paragraph -->
<USkeleton class="h-4 w-3/4" /> <!-- Matches paragraph -->
</div>
<!-- Actual content -->
<div v-else>
<h1 class="h-12 w-64 mb-4">{{ title }}</h1>
<p class="h-4 w-full mb-2">{{ paragraph1 }}</p>
<p class="h-4 w-3/4">{{ paragraph2 }}</p>
</div>
</template>Card skeleton example:
<UCard v-if="loading">
<USkeleton class="h-40 w-full mb-4" /> <!-- Image -->
<USkeleton class="h-6 w-3/4 mb-2" /> <!-- Title -->
<USkeleton class="h-4 w-full" /> <!-- Description -->
</UCard>---
12. Card Slots Not Working
Error: Card header/footer not rendering
Symptoms:
- Header or footer content missing
- Slots don't appear
- Layout broken
Root Cause: Incorrect slot syntax or slot names.
Solution: Use proper named slot syntax:
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3>Card Header</h3>
<UButton size="xs">Action</UButton>
</div>
</template>
<!-- Default slot: card body -->
<p>This is the main card content</p>
<template #footer>
<div class="flex gap-2">
<UButton>Cancel</UButton>
<UButton color="primary">Save</UButton>
</div>
</template>
</UCard>Available slots:
header: Top sectiondefault: Main content (unnamed slot)footer: Bottom section
---
13. Avatar Fallback Missing
Error: Broken image icon when avatar image fails to load
Symptoms:
- Broken image placeholder
- No fallback text
- Poor UX for missing avatars
Root Cause: No alt text provided for fallback initials.
Solution: Add alt text (generates initials automatically):
<!-- ✓ With fallback -->
<UAvatar
src="/nonexistent.jpg"
alt="John Doe"
/>
<!-- Shows "JD" if image fails -->
<!-- ✗ Without fallback -->
<UAvatar src="/nonexistent.jpg" />
<!-- Shows broken image icon -->Avatar with explicit initials:
<UAvatar :text="getUserInitials(user.name)" />Avatar groups:
<UAvatarGroup :max="3">
<UAvatar v-for="user in users" :key="user.id" :alt="user.name" />
</UAvatarGroup>---
14. Badge Positioning Wrong
Error: Badge not positioned correctly on buttons/avatars
Symptoms:
- Badge inside element instead of overlaid
- Badge not in corner
- Badge positioning breaks on resize
Root Cause: Missing relative positioning on parent element.
Solution: Use relative positioning on parent:
<!-- Notification badge on button -->
<div class="relative">
<UButton>Messages</UButton>
<UBadge class="absolute -top-2 -right-2" color="red">5</UBadge>
</div>
<!-- Status badge on avatar -->
<div class="relative">
<UAvatar src="/avatar.jpg" alt="User" />
<UBadge class="absolute bottom-0 right-0" color="green" />
</div>Using `position` prop (Nuxt UI v4+):
<UBadge position="top-right" color="red">New</UBadge>---
15. Form Nested Prop Missing
Error: Nested form validation fails or doesn't trigger
Symptoms:
- Inner form validation not working
- Submit button doesn't respect nested validation
- Console errors about duplicate form context
Root Cause: Missing nested prop on inner <UForm>.
Solution: Add nested prop to inner forms:
<UForm :state="outerState" @submit="onSubmit">
<UFormGroup label="Parent Field">
<UInput v-model="outerState.parentField" />
</UFormGroup>
<!-- Nested form -->
<UForm :state="innerState" nested>
<UFormGroup label="Child Field">
<UInput v-model="innerState.childField" />
</UFormGroup>
</UForm>
<UButton type="submit">Submit All</UButton>
</UForm>Why this works: The nested prop prevents the inner form from creating its own submit context.
---
16. Table Pagination State
Error: Pagination controls not updating when page changes
Symptoms:
- Page number doesn't update
- Can't navigate between pages
pageprop not reactive
Root Cause: Not using v-model for pagination state.
Solution: Use v-model for reactive pagination:
<template>
<UTable
v-model:page="page"
v-model:page-count="pageCount"
:rows="paginatedRows"
:columns="columns"
>
<template #pagination>
<UPagination
v-model="page"
:page-count="pageCount"
:total="totalRows"
/>
</template>
</UTable>
</template>
<script setup lang="ts">
const page = ref(1)
const pageSize = 10
const totalRows = computed(() => allRows.value.length)
const pageCount = computed(() => Math.ceil(totalRows.value / pageSize))
const paginatedRows = computed(() => {
const start = (page.value - 1) * pageSize
return allRows.value.slice(start, start + pageSize)
})
</script>---
17. Color Mode Not Persisting
Error: Dark mode setting resets on page reload
Symptoms:
- User's color preference forgotten
- Always starts in light mode
- Toggle doesn't save
Root Cause: This is actually NOT an error - color mode auto-persists by default.
How it works:
const colorMode = useColorMode()
// Automatically persisted to localStorage
colorMode.value = 'dark'
// Preference key: 'nuxt-color-mode'If persistence isn't working, check: 1. localStorage is enabled (not in incognito mode) 2. Not overriding with preference: 'system' 3. Browser allows localStorage
Force system preference:
// app.config.ts
export default defineAppConfig({
ui: {
colorMode: {
preference: 'system' // Always use system preference
}
}
})---
18. TypeScript Types Not Generated
Error: TypeScript errors for component types or auto-imports
Symptoms:
- Red squiggles in IDE
- "Cannot find name 'UButton'"
- Auto-imports not working
.nuxt/directory missing types
Root Cause: Types not generated or stale.
Solution: Run type generation:
# Generate types
bunx nuxt prepare
# Or run dev (auto-generates types)
bunx nuxt devPersistent issues:
# Clear cache and regenerate
rm -rf .nuxt
bunx nuxt prepare
# Check tsconfig.json extends
{
"extends": "./.nuxt/tsconfig.json"
}Add to `.gitignore`:
.nuxt/---
19. Theme Variants Not Applying
Error: Custom theme configuration not working
Symptoms:
uiprop changes ignored- Global theme not applied
- Component still uses default theme
Root Cause: Incorrect customization order or syntax.
Customization hierarchy (lowest to highest priority): 1. Global theme (app.config.ts) 2. Component ui prop 3. Tailwind classes
Solution: Check customization order:
<template>
<!-- Method 1: Global theme (lowest priority) -->
<!-- See app.config.ts -->
<!-- Method 2: ui prop (medium priority) -->
<UButton
:ui="{
base: 'font-bold',
variant: {
solid: 'bg-custom-500'
}
}"
>
Custom Button
</UButton>
<!-- Method 3: class (highest priority) -->
<UButton class="!bg-red-500 !text-white">
Override All
</UButton>
</template>Global theme (app.config.ts):
export default defineAppConfig({
ui: {
button: {
base: 'font-semibold',
variant: {
solid: 'bg-primary-600 hover:bg-primary-700'
}
}
}
})---
20. Responsive Patterns Broken
Error: Mobile layouts not working as expected
Symptoms:
- Grid doesn't stack on mobile
- Text too small on mobile
- Buttons too close together
- Horizontal scroll on mobile
Root Cause: Not using Tailwind responsive utilities.
Solution: Use responsive breakpoint prefixes:
<template>
<!-- Responsive grid: 1 col mobile, 2 tablet, 3 desktop -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<UCard v-for="item in items" :key="item.id">
{{ item.title }}
</UCard>
</div>
<!-- Responsive text sizes -->
<h1 class="text-2xl md:text-3xl lg:text-4xl">
Responsive Heading
</h1>
<!-- Responsive spacing -->
<div class="p-4 md:p-6 lg:p-8">
Content with responsive padding
</div>
<!-- Responsive flex direction -->
<div class="flex flex-col md:flex-row gap-4">
<UButton>Action 1</UButton>
<UButton>Action 2</UButton>
</div>
</template>Tailwind breakpoints:
sm: 640pxmd: 768pxlg: 1024pxxl: 1280px2xl: 1536px
Mobile-first approach: Always style mobile first, then add md:, lg: overrides.
---
21. Cannot read property 'focus' of undefined (v4.2+)
Error: TypeError: Cannot read properties of undefined (reading 'focus') or inputRef.value.$el is undefined
Symptoms:
- Template refs not working after upgrading to v4.2.0+
.$elaccessor returns undefined- TypeScript errors on component refs
- Focus/scroll operations failing
Root Cause: Breaking change in v4.2.0 - InputMenu, InputNumber, and SelectMenu now expose HTML elements directly instead of component instances.
Affected Components:
UInputMenuUInputNumberUSelectMenu
Solution: Remove the .$el accessor and use direct element access:
Before (v4.0/v4.1) ❌:
<template>
<UInputMenu ref="inputRef" />
</template>
<script setup lang="ts">
const inputRef = ref()
onMounted(() => {
// Old way - component instance
inputRef.value.$el.focus() // ❌ Breaks in v4.2+
})
</script>After (v4.2+) ✅:
<template>
<UInputMenu ref="inputRef" />
</template>
<script setup lang="ts">
const inputRef = ref<HTMLElement>()
onMounted(() => {
// New way - direct element access
inputRef.value?.focus() // ✅ Works in v4.2+
})
</script>Migration steps: 1. Search codebase for .$el usage on InputMenu, InputNumber, SelectMenu 2. Remove all .$el accessors 3. Update TypeScript types from component refs to HTMLElement 4. Test all imperative DOM operations (focus, scroll, etc.)
Complete migration example:
<template>
<div class="space-y-4">
<UInputMenu ref="menuRef" />
<UInputNumber ref="numberRef" />
<USelectMenu ref="selectRef" />
<UButton @click="focusAll">Focus All</UButton>
</div>
</template>
<script setup lang="ts">
// ✅ Correct types for v4.2+
const menuRef = ref<HTMLElement>()
const numberRef = ref<HTMLElement>()
const selectRef = ref<HTMLElement>()
function focusAll() {
// ✅ Direct element access
menuRef.value?.focus()
numberRef.value?.focus()
selectRef.value?.focus()
// ✅ Scrolling also works
menuRef.value?.scrollIntoView({ behavior: 'smooth' })
}
</script>Why this change?: Consistency with other Nuxt UI components and HTML standards. Most components already exposed HTML elements directly.
See also: nuxt-v4-features.md for complete v4.2.x migration guide
---
22. Missing tailwindcss Package
Error: "Cannot find module tailwindcss" or styles not building
Symptoms:
- Build errors about missing tailwindcss
@import "tailwindcss"fails- CSS not compiling
Root Cause: Nuxt UI v4.6+ requires tailwindcss as an explicit peer dependency.
Solution: Install both packages:
bun add @nuxt/ui tailwindcssWhy: Earlier versions auto-installed tailwindcss. v4.6+ requires explicit installation for better dependency management.
---
23. useChat Not Found (AI SDK v5)
Error: useChat is not exported from '@ai-sdk/vue'
Symptoms:
- Import error for
useChat - Chat state not reactive
- TypeScript error on composable
Root Cause: AI SDK v5 replaced useChat with the Chat class.
Before (v4) ❌:
import { useChat } from '@ai-sdk/vue'
const { messages, input, handleSubmit, status } = useChat()After (v5) ✅:
import { Chat } from '@ai-sdk/vue'
const chat = new Chat({
onError(error) { console.error(error) }
})
// chat.messages, chat.status, chat.sendMessage()---
24. Chat messages.content Not Working
Error: Chat messages display empty or [object Object]
Symptoms:
message.contentis undefined- Messages show as blank
- Tool calls and reasoning not rendering
Root Cause: AI SDK v5 uses message.parts instead of message.content.
Before ❌:
<template #content="{ message }">
{{ message.content }}
</template>After ✅:
<script setup lang="ts">
import { isTextUIPart, isReasoningUIPart, isToolUIPart, getToolName } from 'ai'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
</script>
<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="isReasoningStreaming(message, index, chat)" />
<UChatTool v-else-if="isToolUIPart(part)" :text="getToolName(part)" :streaming="isToolStreaming(part)" />
<template v-else-if="isTextUIPart(part)">
<MDC v-if="message.role === 'assistant'" :value="part.text" />
<p v-else class="whitespace-pre-wrap">{{ part.text }}</p>
</template>
</template>
</template>---
25. Form Nested Validation Not Inherited
Error: Nested form validation not triggering or state not syncing with parent
Symptoms:
- Child form doesn't validate when parent submits
- Nested field errors don't display
- Parent form doesn't see child form state
Root Cause: v4 requires explicit nested prop AND name prop on child forms.
Solution:
<UForm :state="state" :schema="schema" @submit="onSubmit">
<UFormField label="Customer" name="customer">
<UInput v-model="state.customer" />
</UFormField>
<div v-for="(item, index) in state.items" :key="index">
<UForm
:name="`items.${index}`"
:schema="itemSchema"
nested
>
<UFormField name="description">
<UInput v-model="item.description" />
</UFormField>
</UForm>
</div>
</UForm>Key changes from v3:
nestedprop is now required (not automatic)nameprop on child form matches parent field path (e.g.,items.0)- Schema transformations only apply to submit data, not internal state
---
See also:
ai-sdk-v5-integration.mdfor complete AI SDK migration guidechat-components.mdfor chat component referencenuxt-v4-features.mdfor v4 migration features
Common Components Reference
Top 43 Most Used Components
Forms
- Input - Text input fields
- InputDate (v4.2+) - Date picker with calendar UI and range selection
- InputTime (v4.2+) - Time picker with 12/24-hour format
- Select - Dropdown selections
- Checkbox - Boolean selections
- Radio - Single choice from options
- Textarea - Multi-line text
- Switch - Toggle boolean
Navigation
- Tabs - Content organization
- Breadcrumb - Navigation paths
- CommandPalette - Search & commands
- Pagination - List navigation
Overlays
- Modal - Full overlays
- Drawer - Side panels
- Dialog - Confirmations
- Popover - Rich tooltips
- DropdownMenu - Action menus
- Tooltip - Hover info
Feedback
- Alert - Messages
- Toast - Notifications
- Skeleton - Loading states
- Progress - Progress indicators
- Empty (v4.2+) - Empty state display with icon and actions
Layout
- Card - Content containers
- Container - Max-width wrapper
- Avatar - User images
- Badge - Status indicators
Data
- Table - Data display
See SKILL.md for detailed usage examples.
Component Theming Guide
Customization Hierarchy (Order of Specificity)
1. Global Config (app.config.ts) 2. Component `ui` Prop (per-instance) 3. Slot `class` Prop (per-element)
Global Theming
export default defineAppConfig({
ui: {
theme: {
defaultVariants: {
Button: { size: 'md', color: 'primary' },
Input: { size: 'md', variant: 'outline' }
}
}
}
})Component-Level
<UButton :ui="{ base: 'font-bold', rounded: 'rounded-full' }">
Custom Button
</UButton>Slot-Level
<UCard :ui="{ header: 'bg-primary text-white' }">
<template #header>Header</template>
</UCard>---
Understanding Slots
What Are Slots?
Slots are distinct HTML elements or sections within a component. Each slot can be styled independently.
Example: UCard Component Structure
<UCard>
<!-- root slot: The outer container -->
<div class="root-slot">
<!-- header slot: Optional header section -->
<div class="header-slot">
<slot name="header" />
</div>
<!-- body slot: Main content area -->
<div class="body-slot">
<slot /> <!-- Default slot -->
</div>
<!-- footer slot: Optional footer section -->
<div class="footer-slot">
<slot name="footer" />
</div>
</div>
</UCard>Typical Slot Names
Most components follow these patterns:
Container Components (Card, Modal, Drawer):
root- Outer containerheader- Top sectionbody- Main contentfooter- Bottom section
Form Components (Input, Select):
root- Wrapper elementbase- Input element itselfleading- Icon/content before inputtrailing- Icon/content after inputlabel- Label elementhint- Helper texterror- Error message
Button Component:
rootorbase- Button elementleading- Leading icon slotlabel- Text label slottrailing- Trailing icon slot
Styling Individual Slots
<template>
<UCard
:ui="{
root: 'shadow-lg border-2 border-primary',
header: 'bg-primary text-white p-6',
body: 'p-6 bg-elevated',
footer: 'bg-muted p-4 border-t border-default'
}"
>
<template #header>
<h2>Card Title</h2>
</template>
<p>Card content goes here</p>
<template #footer>
<UButton>Action</UButton>
</template>
</UCard>
</template>Result: Each section has its own independent styling.
---
Variants System
What Are Variants?
Variants map component props to style definitions. They enable dynamic styling based on prop values.
Example: Avatar Size Variants
variants: {
size: {
xs: { root: 'size-6 text-xs' },
sm: { root: 'size-7 text-sm' },
md: { root: 'size-8 text-base' }, // default
lg: { root: 'size-9 text-lg' },
xl: { root: 'size-10 text-xl' }
}
}Usage:
<template>
<UAvatar size="sm" /> <!-- Uses 'sm' variant: size-7 text-sm -->
<UAvatar size="lg" /> <!-- Uses 'lg' variant: size-9 text-lg -->
<UAvatar /> <!-- Uses default 'md': size-8 text-base -->
</template>How Variants Work
1. Component receives prop (e.g., size="lg") 2. Variant system looks up the size variant 3. Applies corresponding classes to relevant slots
Multiple Variants Example
Components often have multiple variant props:
// Button variants
variants: {
size: {
xs: { root: 'px-2 py-1 text-xs' },
sm: { root: 'px-3 py-1.5 text-sm' },
md: { root: 'px-4 py-2 text-base' },
lg: { root: 'px-5 py-2.5 text-lg' }
},
variant: {
solid: { root: 'bg-primary text-white' },
outline: { root: 'border border-primary text-primary bg-transparent' },
ghost: { root: 'bg-transparent text-primary hover:bg-primary/10' },
link: { root: 'bg-transparent text-primary underline' }
},
color: {
primary: { root: 'theme-primary' },
success: { root: 'theme-success' },
error: { root: 'theme-error' }
}
}Usage:
<template>
<!-- Combines size + variant + color variants -->
<UButton
size="lg"
variant="outline"
color="success"
>
Large Outline Success Button
</UButton>
</template>Default Variants
Set defaults globally for all component instances:
// app.config.ts
export default defineAppConfig({
ui: {
theme: {
defaultVariants: {
Button: {
size: 'md', // All buttons default to medium
variant: 'solid', // All buttons default to solid
color: 'primary' // All buttons default to primary color
},
Input: {
size: 'md',
variant: 'outline'
}
}
}
}
})Now every <UButton> uses these defaults unless overridden:
<UButton> <!-- md + solid + primary -->
<UButton size="lg"> <!-- lg + solid + primary -->
<UButton variant="ghost"> <!-- md + ghost + primary -->---
Compound Variants
What Are Compound Variants?
Compound variants apply styles when multiple conditions are met simultaneously.
Example: Button with Primary Color + Large Size
compoundVariants: [
{
color: 'primary',
size: 'lg',
class: 'shadow-lg font-semibold'
},
{
variant: 'outline',
color: 'error',
class: 'border-2 hover:bg-error/10'
}
]Result:
<!-- Applies compound variant: shadow-lg font-semibold -->
<UButton color="primary" size="lg">
Large Primary Button
</UButton>
<!-- Applies compound variant: border-2 hover:bg-error/10 -->
<UButton variant="outline" color="error">
Outline Error Button
</UButton>
<!-- No compound variant applied -->
<UButton color="primary" size="md">
Medium Primary Button
</UButton>Use Cases for Compound Variants
1. Enhanced Emphasis
compoundVariants: [
{
color: 'error',
variant: 'solid',
class: 'shadow-error-md animate-pulse' // Extra attention for destructive actions
}
]2. Responsive Adjustments
compoundVariants: [
{
size: 'lg',
variant: 'solid',
class: 'md:px-8 md:py-4' // Larger padding on desktop for large solid buttons
}
]3. Accessibility Enhancements
compoundVariants: [
{
variant: 'ghost',
color: 'neutral',
class: 'focus:ring-2 focus:ring-offset-2' // Extra focus visibility for subtle buttons
}
]---
class vs ui Prop
Critical Distinction
`class` Prop:
- Targets root/base slot ONLY
- Simple string of classes
- Cannot target other slots
`ui` Prop:
- Targets ANY slot
- Object with slot names as keys
- Full component customization
Examples
<template>
<!-- ✅ class prop: Styles root element only -->
<UButton class="w-full">
Full Width Button
</UButton>
<!-- Result: <button class="w-full ...">...</button> -->
<!-- ✅ ui prop: Styles multiple slots -->
<UButton
:ui="{
root: 'w-full',
leading: 'size-5',
label: 'font-bold'
}"
>
<template #leading>
<UIcon name="i-lucide-star" />
</template>
Customized Button
</UButton>
<!-- ❌ WRONG: class prop cannot target leading slot -->
<UButton class="leading:size-5">
<!-- This won't work -->
</UButton>
</template>When to Use Each
Use `class` when:
- Styling root element only
- Simple utility class additions
- Quick one-off styling
Use `ui` when:
- Customizing multiple slots
- Targeting specific internal elements
- Complex component styling
- Building reusable variants
Combining Both
You can use both together:
<template>
<UButton
class="w-full md:w-auto"
:ui="{
root: 'shadow-lg',
label: 'font-semibold'
}"
>
Hybrid Styling
</UButton>
</template>Result: class and ui.root both apply to root element, ui.label applies to label slot.
---
Vue-Only Configuration
For Vue (Not Nuxt) Projects
If using Vue without Nuxt, configure in vite.config.ts:
vite.config.ts:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
// Theme configuration
colors: {
primary: 'indigo',
secondary: 'purple',
success: 'green',
error: 'red'
},
// Default variants
theme: {
defaultVariants: {
Button: {
size: 'md',
variant: 'solid'
}
}
},
// Global component styling
button: {
slots: {
root: 'font-semibold transition-all',
leading: 'size-5'
},
variants: {
size: {
xs: { root: 'px-2 py-1 text-xs' },
sm: { root: 'px-3 py-1.5 text-sm' },
md: { root: 'px-4 py-2 text-base' },
lg: { root: 'px-6 py-3 text-lg' }
}
}
}
}
})
]
})Nuxt vs Vue Configuration
| Feature | Nuxt | Vue (vite.config.ts) |
|---|---|---|
| Runtime Colors | app.config.ts | vite.config.ts |
| Default Variants | app.config.ts | vite.config.ts |
| Global Slots | app.config.ts | vite.config.ts |
| Hot Reload | Yes | Requires restart |
---
Advanced Patterns
Conditional Slot Styling
<script setup lang="ts">
const hasError = ref(false)
const cardUi = computed(() => ({
root: hasError.value ? 'border-2 border-error' : 'border border-default',
header: hasError.value ? 'bg-error/10 text-error' : 'bg-default'
}))
</script>
<template>
<UCard :ui="cardUi">
<template #header>
{{ hasError ? 'Error State' : 'Normal State' }}
</template>
<p>Content</p>
</UCard>
</template>Extending Component Variants
// app.config.ts
export default defineAppConfig({
ui: {
button: {
variants: {
// Add custom size
size: {
'2xl': { root: 'px-8 py-4 text-2xl' }
},
// Add custom variant
variant: {
gradient: {
root: 'bg-gradient-to-r from-primary to-secondary text-white'
}
}
}
}
}
})Usage:
<UButton size="2xl" variant="gradient">
Custom Gradient Button
</UButton>Responsive Variants with Tailwind
<template>
<UButton
:ui="{
root: 'w-full md:w-auto',
label: 'text-sm md:text-base'
}"
>
Responsive Button
</UButton>
</template>---
Best Practices
1. Prefer Global Defaults
// ✅ GOOD: Set once globally
// app.config.ts
defaultVariants: {
Button: { size: 'md', variant: 'solid' }
}
// ❌ AVOID: Repeating on every instance
<UButton size="md" variant="solid">...</UButton>
<UButton size="md" variant="solid">...</UButton>
<UButton size="md" variant="solid">...</UButton>2. Use ui Prop for Complex Styling
<!-- ✅ GOOD: Use ui prop for multi-slot styling -->
<UCard
:ui="{
root: 'shadow-xl',
header: 'bg-primary text-white',
body: 'prose'
}"
/>
<!-- ❌ AVOID: class prop can't target slots -->
<UCard class="shadow-xl header:bg-primary body:prose" />3. Create Composables for Reusable Styles
// composables/useCardStyles.ts
export const useCardStyles = (variant: 'default' | 'error' | 'success') => {
const styles = {
default: {
root: 'border border-default',
header: 'bg-elevated'
},
error: {
root: 'border-2 border-error',
header: 'bg-error/10 text-error'
},
success: {
root: 'border-2 border-success',
header: 'bg-success/10 text-success'
}
}
return styles[variant]
}Usage:
<script setup lang="ts">
const cardStyles = useCardStyles('error')
</script>
<template>
<UCard :ui="cardStyles">
<template #header>Error Card</template>
<p>Content</p>
</UCard>
</template>4. Document Custom Variants
When adding custom variants, document them:
// app.config.ts
export default defineAppConfig({
ui: {
button: {
variants: {
/**
* Custom button sizes
* - xs: Extra small (mobile)
* - 2xl: Extra large (hero CTAs)
*/
size: {
xs: { root: 'px-1.5 py-0.5 text-xs' },
'2xl': { root: 'px-8 py-4 text-2xl' }
}
}
}
}
})---
Troubleshooting
Styles Not Applying
Problem: ui prop styles don't appear.
Solution: Check slot names match component structure. Use browser DevTools to inspect actual slot names.
Conflicting Styles
Problem: Global and component styles conflict.
Solution: Remember the hierarchy: 1. Global config (lowest priority) 2. Component ui prop 3. Slot class prop (highest priority)
Variant Not Working
Problem: Custom variant doesn't apply.
Solution: Ensure variant is registered in global config and prop name matches:
// app.config.ts
ui: {
button: {
variants: {
customSize: { // ← Must match prop name
huge: { root: 'px-10 py-6' }
}
}
}
}<UButton customSize="huge"> <!-- ← Must match config -->---
Last Updated: 2025-01-09 Nuxt UI Version: 4.0.0
Composables Guide
useToast
const { add, remove, clear } = useToast()
// Add toast
add({
title: 'Success',
description: 'Item saved',
color: 'success',
timeout: 3000,
actions: [{
label: 'Undo',
click: () => console.log('Undo')
}]
})
// Remove specific toast
remove(toastId)
// Clear all
clear()useNotification
Similar to useToast but for persistent notifications.
useColorMode
const colorMode = useColorMode()
// Get current mode
console.log(colorMode.value) // 'light' | 'dark' | 'system'
// Set mode
colorMode.preference = 'dark'
// Check if dark
const isDark = computed(() => colorMode.value === 'dark')defineShortcuts
defineShortcuts({
'meta_k': () => openCommandPalette(),
'meta_n': () => createNew(),
'escape': () => closeModal()
})Supports: meta (⌘), ctrl, alt, shift, and standard keys.
Content Components Reference
Nuxt UI v4 provides components for documentation sites and blogs.
Component Overview
| Component | Purpose |
|---|---|
| BlogPost | Article display |
| BlogPosts | Blog grid |
| ChangelogVersion | Version entry |
| ChangelogVersions | Version timeline |
| ContentNavigation | Doc navigation |
| ContentSearch | Doc search |
| ContentSearchButton | Search button |
| ContentSurround | Prev/next links |
| ContentToc | Table of contents |
Blog Components
BlogPost
Individual blog post display.
interface BlogPostProps {
title: string
description?: string
date?: string | Date
image?: string
badge?: BadgeProps
authors?: Author[]
to?: string
orientation?: 'horizontal' | 'vertical'
}
interface Author {
name: string
avatar?: string
to?: string
}<UBlogPost
title="Introducing Nuxt UI v4"
description="The biggest update yet with 125+ components"
date="2024-11-15"
image="/blog/nuxt-ui-v4.png"
:badge="{ label: 'Release', color: 'primary' }"
:authors="[
{ name: 'John Doe', avatar: '/avatars/john.jpg' }
]"
to="/blog/nuxt-ui-v4"
/>BlogPosts
Grid layout for blog posts.
<script setup>
const posts = await queryContent('/blog').find()
</script>
<template>
<UBlogPosts>
<UBlogPost
v-for="post in posts"
:key="post._path"
:title="post.title"
:description="post.description"
:date="post.date"
:image="post.image"
:authors="post.authors"
:to="post._path"
/>
</UBlogPosts>
</template>Changelog Components
ChangelogVersion
Single version entry.
interface ChangelogVersionProps {
title: string
date?: string | Date
icon?: string
badge?: BadgeProps
to?: string
}<UChangelogVersion
title="v4.2.0"
date="2024-11-20"
icon="i-heroicons-rocket-launch"
:badge="{ label: 'Latest', color: 'success' }"
>
<h3>New Features</h3>
<ul>
<li>InputDate component</li>
<li>InputTime component</li>
<li>Empty state component</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Fixed modal focus trap</li>
<li>Fixed table virtualization</li>
</ul>
</UChangelogVersion>ChangelogVersions
Timeline of versions.
<script setup>
const versions = [
{
title: 'v4.2.0',
date: '2024-11-20',
badge: { label: 'Latest', color: 'success' },
content: '...'
},
{
title: 'v4.1.0',
date: '2024-10-15',
content: '...'
}
]
</script>
<template>
<UChangelogVersions>
<UChangelogVersion
v-for="version in versions"
:key="version.title"
v-bind="version"
>
<MDC :value="version.content" />
</UChangelogVersion>
</UChangelogVersions>
</template>Documentation Components
ContentNavigation
Accordion-style documentation navigation.
interface ContentNavigationProps {
items: NavigationItem[]
defaultOpen?: boolean
multiple?: boolean // Allow multiple open
}
interface NavigationItem {
label: string
icon?: string
to?: string
children?: NavigationItem[]
defaultOpen?: boolean
}<script setup>
const navigation = [
{
label: 'Getting Started',
icon: 'i-heroicons-home',
defaultOpen: true,
children: [
{ label: 'Introduction', to: '/docs' },
{ label: 'Installation', to: '/docs/installation' },
{ label: 'Configuration', to: '/docs/configuration' }
]
},
{
label: 'Components',
icon: 'i-heroicons-cube',
children: [
{ label: 'Button', to: '/docs/components/button' },
{ label: 'Card', to: '/docs/components/card' }
]
}
]
</script>
<template>
<UContentNavigation :items="navigation" />
</template>ContentSearch
CommandPalette for documentation search.
<script setup>
const searchGroups = computed(() => [
{
key: 'pages',
label: 'Pages',
commands: pages.value.map(page => ({
id: page._path,
label: page.title,
to: page._path
}))
}
])
</script>
<template>
<UContentSearch :groups="searchGroups" />
</template>ContentSearchButton
Button to trigger search.
<UContentSearchButton />
<!-- With custom styling -->
<UContentSearchButton class="w-full">
<template #default>
<span>Search documentation...</span>
<UKbd>⌘K</UKbd>
</template>
</UContentSearchButton>ContentToc
Sticky table of contents with active link highlighting.
interface ContentTocProps {
links: TocLink[]
title?: string
}
interface TocLink {
id: string
text: string
depth: number
children?: TocLink[]
}<script setup>
// With @nuxt/content
const { data: page } = await useAsyncData('page', () => {
return queryContent(route.path).findOne()
})
</script>
<template>
<UContentToc :links="page?.body?.toc?.links" title="On this page" />
</template>ContentSurround
Previous/next navigation links.
<script setup>
const { data: surround } = await useAsyncData('surround', () => {
return queryContent()
.only(['_path', 'title', 'description'])
.findSurround(route.path)
})
</script>
<template>
<UContentSurround :surround="surround" />
</template>Documentation Layout
Complete documentation page layout:
<!-- layouts/docs.vue -->
<template>
<UPage>
<template #left>
<UPageAside>
<UContentSearchButton class="mb-4" />
<UContentNavigation :items="navigation" />
</UPageAside>
</template>
<UPageBody>
<UPageHeader
:title="page?.title"
:description="page?.description"
/>
<slot />
<UContentSurround :surround="surround" />
</UPageBody>
<template #right>
<UPageAside>
<UContentToc :links="page?.body?.toc?.links" />
</UPageAside>
</template>
</UPage>
<UContentSearch :groups="searchGroups" />
</template>Blog Layout
Complete blog index:
<!-- pages/blog/index.vue -->
<template>
<UPage>
<UPageHeader
title="Blog"
description="Latest news and updates"
/>
<UBlogPosts>
<UBlogPost
v-for="post in posts"
:key="post._path"
v-bind="post"
:to="post._path"
/>
</UBlogPosts>
<UPagination
v-model="page"
:total="total"
:per-page="10"
/>
</UPage>
</template>Theming
export default defineAppConfig({
ui: {
blogPost: {
slots: {
root: 'group flex flex-col',
image: 'aspect-video rounded-lg overflow-hidden',
body: 'flex-1 flex flex-col',
title: 'text-xl font-semibold group-hover:text-primary',
description: 'text-muted mt-2'
}
},
contentNavigation: {
slots: {
root: 'space-y-1',
item: 'flex items-center gap-2 px-3 py-2 rounded-lg',
itemActive: 'bg-primary/10 text-primary'
}
},
contentToc: {
slots: {
root: 'space-y-2',
link: 'block text-sm text-muted hover:text-default',
linkActive: 'text-primary font-medium'
}
}
}
})Nuxt Content Integration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxt/content'],
content: {
highlight: {
theme: 'github-dark'
}
}
})With @nuxt/content, you get automatic:
- Markdown processing with MDC
- Syntax highlighting
- Table of contents generation
- Navigation from file structure
- Search indexing
CSS Variables Reference - Nuxt UI v4
Complete reference for all CSS variables in Nuxt UI v4 and how to use them for custom styling.
---
Overview
Nuxt UI v4 uses CSS variables for theming, enabling:
- Runtime customization without recompilation
- Dark mode automatic adaptation
- Semantic naming for consistent design
- Direct CSS usage in custom components
All variables automatically adjust for light/dark color modes.
---
Semantic Color Variables
Primary, Secondary, and Semantic Colors
These map to your configured theme colors and adjust between light/dark modes.
Light Mode:
:root {
--ui-primary: var(--ui-color-primary-500);
--ui-secondary: var(--ui-color-secondary-500);
--ui-success: var(--ui-color-success-500);
--ui-info: var(--ui-color-info-500);
--ui-warning: var(--ui-color-warning-500);
--ui-error: var(--ui-color-error-500);
--ui-neutral: var(--ui-color-neutral-500);
}Dark Mode:
.dark {
--ui-primary: var(--ui-color-primary-400);
--ui-secondary: var(--ui-color-secondary-400);
--ui-success: var(--ui-color-success-400);
--ui-info: var(--ui-color-info-400);
--ui-warning: var(--ui-color-warning-400);
--ui-error: var(--ui-color-error-400);
--ui-neutral: var(--ui-color-neutral-400);
}Note: Dark mode shifts from 500 → 400 for better contrast on dark backgrounds.
Usage in Custom CSS
/* Custom button using semantic colors */
.my-custom-button {
background-color: var(--ui-primary);
color: white;
border: 1px solid var(--ui-primary);
}
.my-custom-button:hover {
background-color: color-mix(in srgb, var(--ui-primary) 90%, black);
}
/* Success state */
.success-badge {
background-color: var(--ui-success);
color: white;
}
/* Error message */
.error-text {
color: var(--ui-error);
}---
Text Color Variables
Used for typography with semantic naming based on emphasis level.
Complete Text Variable List
Light Mode:
:root {
--ui-text-highlighted: var(--ui-color-neutral-900);
--ui-text: var(--ui-color-neutral-700);
--ui-text-toned: var(--ui-color-neutral-600);
--ui-text-muted: var(--ui-color-neutral-500);
--ui-text-dimmed: var(--ui-color-neutral-400);
--ui-text-inverted: white;
}Dark Mode:
.dark {
--ui-text-highlighted: white;
--ui-text: var(--ui-color-neutral-200);
--ui-text-toned: var(--ui-color-neutral-300);
--ui-text-muted: var(--ui-color-neutral-400);
--ui-text-dimmed: var(--ui-color-neutral-500);
--ui-text-inverted: var(--ui-color-neutral-900);
}Utility Class Mapping
| CSS Variable | Utility Class | Use Case |
|---|---|---|
--ui-text-highlighted | text-highlighted | Primary headings, emphasized text |
--ui-text | text-default | Body text, default paragraphs |
--ui-text-toned | text-toned | Slightly subdued text |
--ui-text-muted | text-muted | Secondary information |
--ui-text-dimmed | text-dimmed | Tertiary info, placeholders |
--ui-text-inverted | text-inverted | Text on dark backgrounds |
Usage Examples
<template>
<!-- Using utility classes -->
<h1 class="text-highlighted">Main Heading</h1>
<p class="text-default">Body paragraph text.</p>
<span class="text-muted">Secondary info</span>
<small class="text-dimmed">Helper text</small>
<!-- Using CSS variables directly -->
<div :style="{ color: 'var(--ui-text-muted)' }">
Custom styled text
</div>
</template>
<style scoped>
/* Custom component using variables */
.my-label {
color: var(--ui-text);
font-weight: 500;
}
.my-description {
color: var(--ui-text-muted);
font-size: 0.875rem;
}
.my-placeholder {
color: var(--ui-text-dimmed);
}
</style>---
Background Color Variables
Used for surfaces, cards, and container backgrounds.
Complete Background Variable List
Light Mode:
:root {
--ui-bg: white;
--ui-bg-elevated: var(--ui-color-neutral-50);
--ui-bg-muted: var(--ui-color-neutral-50);
--ui-bg-accented: var(--ui-color-neutral-100);
--ui-bg-inverted: var(--ui-color-neutral-900);
}Dark Mode:
.dark {
--ui-bg: var(--ui-color-neutral-950);
--ui-bg-elevated: var(--ui-color-neutral-900);
--ui-bg-muted: var(--ui-color-neutral-900);
--ui-bg-accented: var(--ui-color-neutral-800);
--ui-bg-inverted: white;
}Utility Class Mapping
| CSS Variable | Utility Class | Use Case |
|---|---|---|
--ui-bg | bg-default | Page background |
--ui-bg-elevated | bg-elevated | Cards, modals (higher elevation) |
--ui-bg-muted | bg-muted | Subtle backgrounds |
--ui-bg-accented | bg-accented | More prominent backgrounds |
--ui-bg-inverted | bg-inverted | Inverse backgrounds |
Additional Background Variables
:root {
--ui-bg-overlay: rgba(0, 0, 0, 0.5); /* Modal/dialog overlays */
--ui-bg-disabled: var(--ui-color-neutral-100);
}
.dark {
--ui-bg-overlay: rgba(0, 0, 0, 0.75);
--ui-bg-disabled: var(--ui-color-neutral-800);
}Usage Examples
<template>
<!-- Using utility classes -->
<div class="bg-default">Page container</div>
<div class="bg-elevated">Card with elevation</div>
<div class="bg-accented">Highlighted section</div>
</template>
<style scoped>
/* Custom card component */
.custom-card {
background-color: var(--ui-bg-elevated);
padding: 1.5rem;
border-radius: var(--ui-radius);
}
/* Hover state */
.custom-card:hover {
background-color: var(--ui-bg-accented);
}
/* Modal overlay */
.modal-backdrop {
background-color: var(--ui-bg-overlay);
}
</style>---
Border Color Variables
Used for borders, dividers, and outlines.
Complete Border Variable List
Light Mode:
:root {
--ui-border: var(--ui-color-neutral-200);
--ui-border-muted: var(--ui-color-neutral-200);
--ui-border-accented: var(--ui-color-neutral-300);
--ui-border-inverted: var(--ui-color-neutral-900);
}Dark Mode:
.dark {
--ui-border: var(--ui-color-neutral-800);
--ui-border-muted: var(--ui-color-neutral-800);
--ui-border-accented: var(--ui-color-neutral-700);
--ui-border-inverted: white;
}Utility Class Mapping
| CSS Variable | Utility Class | Use Case |
|---|---|---|
--ui-border | border-default | Standard borders |
--ui-border-muted | border-muted | Subtle dividers |
--ui-border-accented | border-accented | Emphasized borders |
--ui-border-inverted | border-inverted | Inverse borders |
Usage Examples
<template>
<!-- Using utility classes -->
<div class="border border-default">Default border</div>
<hr class="border-t border-muted" />
<div class="border-2 border-accented">Emphasized</div>
</template>
<style scoped>
/* Custom divider */
.section-divider {
border-top: 1px solid var(--ui-border-muted);
margin: 2rem 0;
}
/* Input with border */
.custom-input {
border: 1px solid var(--ui-border);
border-radius: var(--ui-radius);
}
.custom-input:focus {
border-color: var(--ui-primary);
outline: none;
}
</style>---
Border Radius System
Nuxt UI v4 uses a single radius variable that scales across all components.
Radius Variable
:root {
--ui-radius: 0.375rem; /* 6px - default */
}Radius Multipliers
All rounded-* utilities scale from --ui-radius:
.rounded-sm { border-radius: calc(var(--ui-radius) * 0.5); } /* 3px */
.rounded { border-radius: var(--ui-radius); } /* 6px */
.rounded-md { border-radius: calc(var(--ui-radius) * 1.33); } /* 8px */
.rounded-lg { border-radius: calc(var(--ui-radius) * 2); } /* 12px */
.rounded-xl { border-radius: calc(var(--ui-radius) * 3); } /* 18px */
.rounded-2xl { border-radius: calc(var(--ui-radius) * 4); } /* 24px */
.rounded-3xl { border-radius: calc(var(--ui-radius) * 6); } /* 36px */
.rounded-full{ border-radius: 9999px; }Customizing Global Radius
Method 1: CSS (Tailwind v4)
/* app.vue <style> */
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--ui-radius: 0.5rem; /* 8px - more rounded */
}Method 2: app.config.ts (if supported)
export default defineAppConfig({
ui: {
theme: {
radius: '0.5rem'
}
}
})Usage Examples
<template>
<!-- Using utility classes -->
<div class="rounded">Standard radius</div>
<div class="rounded-lg">Large radius</div>
<div class="rounded-full">Circular</div>
</template>
<style scoped>
/* Custom component using radius variable */
.custom-card {
border-radius: var(--ui-radius);
}
/* Slightly more rounded */
.custom-button {
border-radius: calc(var(--ui-radius) * 1.5);
}
</style>---
Layout Variables
Used for consistent spacing and sizing across the application.
Container Width
:root {
--ui-container: 80rem; /* 1280px - default max width */
}Usage:
.container {
max-width: var(--ui-container);
margin-inline: auto;
}Header Height
:root {
--ui-header-height: 4rem; /* 64px - default header height */
}Usage:
.main-content {
/* Account for fixed header */
padding-top: var(--ui-header-height);
}
.sticky-header {
height: var(--ui-header-height);
position: sticky;
top: 0;
}Spacing Variables
Tailwind CSS v4 spacing uses CSS variables:
:root {
--spacing-1: 0.25rem; /* 4px */
--spacing-2: 0.5rem; /* 8px */
--spacing-3: 0.75rem; /* 12px */
--spacing-4: 1rem; /* 16px */
--spacing-6: 1.5rem; /* 24px */
--spacing-8: 2rem; /* 32px */
--spacing-12: 3rem; /* 48px */
--spacing-16: 4rem; /* 64px */
/* ... and more */
}---
Ring (Focus) Variables
Used for focus rings and outlines.
:root {
--ui-ring: var(--ui-primary);
--ui-ring-offset: white;
}
.dark {
--ui-ring: var(--ui-primary);
--ui-ring-offset: var(--ui-color-neutral-950);
}Usage:
.custom-input:focus {
outline: 2px solid var(--ui-ring);
outline-offset: 2px;
}
/* Or with Tailwind utilities */
.focus\:ring {
box-shadow: 0 0 0 3px var(--ui-ring);
}---
Shadow Variables
:root {
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}---
Complete Variable Usage Example
<template>
<div class="custom-component">
<div class="custom-header">
<h2>Component Title</h2>
</div>
<div class="custom-body">
<p>Component content</p>
</div>
<div class="custom-footer">
<button class="custom-button">Action</button>
</div>
</div>
</template>
<style scoped>
.custom-component {
/* Layout */
max-width: var(--ui-container);
margin: 0 auto;
/* Background */
background-color: var(--ui-bg-elevated);
/* Border */
border: 1px solid var(--ui-border);
border-radius: var(--ui-radius);
/* Shadow */
box-shadow: var(--shadow-md);
}
.custom-header {
/* Background (slightly different) */
background-color: var(--ui-bg-accented);
/* Border bottom */
border-bottom: 1px solid var(--ui-border-muted);
/* Spacing */
padding: var(--spacing-4) var(--spacing-6);
}
.custom-header h2 {
/* Text */
color: var(--ui-text-highlighted);
margin: 0;
}
.custom-body {
padding: var(--spacing-6);
color: var(--ui-text);
}
.custom-footer {
padding: var(--spacing-4) var(--spacing-6);
border-top: 1px solid var(--ui-border-muted);
background-color: var(--ui-bg-muted);
}
.custom-button {
/* Colors */
background-color: var(--ui-primary);
color: white;
/* Border and radius */
border: none;
border-radius: var(--ui-radius);
/* Spacing */
padding: var(--spacing-2) var(--spacing-4);
/* Cursor */
cursor: pointer;
}
.custom-button:hover {
/* Darken primary color on hover */
background-color: color-mix(in srgb, var(--ui-primary) 90%, black);
}
.custom-button:focus {
/* Focus ring */
outline: 2px solid var(--ui-ring);
outline-offset: 2px;
}
</style>---
Dark Mode Behavior
All CSS variables automatically update when dark mode is enabled.
Automatic Updates:
<template>
<!-- Same code works in both modes -->
<div class="bg-elevated border border-default">
<p class="text-default">This text adapts automatically</p>
</div>
</template>Light Mode Result:
bg-elevated→whiteorneutral-50border-default→neutral-200text-default→neutral-700
Dark Mode Result:
bg-elevated→neutral-900border-default→neutral-800text-default→neutral-200
---
Customizing CSS Variables
Global Customization (Tailwind v4)
/* app.vue <style> or global CSS */
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
/* Override radius */
--ui-radius: 0.5rem;
/* Override container width */
--ui-container: 90rem;
/* Add custom variables */
--my-custom-spacing: 2.5rem;
}Runtime Customization (JavaScript)
<script setup lang="ts">
onMounted(() => {
// Change global radius at runtime
document.documentElement.style.setProperty('--ui-radius', '0.75rem')
// Change container width
document.documentElement.style.setProperty('--ui-container', '100rem')
})
</script>---
Best Practices
1. Use Semantic Variables
/* ✅ GOOD: Semantic meaning */
.error-message {
color: var(--ui-error);
}
/* ❌ AVOID: Direct color values */
.error-message {
color: var(--ui-color-red-500);
}2. Prefer Utility Classes
<!-- ✅ GOOD: Utility classes -->
<div class="bg-elevated border border-default">
<!-- ⚠️ OK but verbose: Inline styles -->
<div :style="{
backgroundColor: 'var(--ui-bg-elevated)',
border: '1px solid var(--ui-border)'
}">3. Use Variables for Custom Components
/* ✅ GOOD: Variables for consistency */
.custom-card {
background: var(--ui-bg-elevated);
border: 1px solid var(--ui-border);
border-radius: var(--ui-radius);
}
/* ❌ AVOID: Hardcoded values */
.custom-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
}4. Dark Mode is Automatic
Don't write manual dark mode selectors when using CSS variables:
/* ✅ GOOD: Automatic dark mode */
.my-component {
background: var(--ui-bg-elevated);
color: var(--ui-text);
}
/* ❌ AVOID: Manual dark mode (unnecessary) */
.my-component {
background: white;
color: #374151;
}
.dark .my-component {
background: #1f2937;
color: #e5e7eb;
}---
Resources
- Tailwind CSS v4 Docs: https://tailwindcss.com/docs/v4-beta
- Nuxt UI Theming: https://ui.nuxt.com/getting-started/theme
- CSS Variables MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
---
Last Updated: 2025-01-09 Nuxt UI Version: 4.0.0 Tailwind CSS Version: 4.0.0
Dark Mode Guide
Purpose: Complete guide to implementing and customizing dark mode in Nuxt UI v4 Feature: Built-in dark mode support with automatic system preference detection
---
Quick Setup
Dark mode is enabled by default in Nuxt UI v4. No additional configuration needed.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
colorMode: true // ← Enabled by default
}
})---
Color Mode Features
Automatic System Detection
On first visit, Nuxt UI automatically detects the user's system preference:
- Uses
prefers-color-schememedia query - Respects OS-level dark mode setting
- No manual user selection required initially
Persistent Preference
User preferences are automatically saved to localStorage:
- Storage key:
nuxt-color-mode - Values:
'light','dark', or'system' - Persistence: Survives page reloads and browser restarts
Three Modes
1. Light: Force light mode 2. Dark: Force dark mode 3. System: Follow OS preference (auto-switches when OS changes)
---
Using useColorMode Composable
Basic Usage
<script setup lang="ts">
const colorMode = useColorMode()
// Get current mode
console.log(colorMode.value) // 'light' | 'dark' | 'system'
// Get preference (what user selected)
console.log(colorMode.preference) // 'light' | 'dark' | 'system'
// Check if currently dark
const isDark = computed(() => colorMode.value === 'dark')
</script>Setting Color Mode
<script setup lang="ts">
const colorMode = useColorMode()
// Set to dark mode
function setDark() {
colorMode.preference = 'dark'
}
// Set to light mode
function setLight() {
colorMode.preference = 'light'
}
// Set to system mode
function setSystem() {
colorMode.preference = 'system'
}
</script>Toggle Color Mode
<template>
<UButton
:icon="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'"
@click="toggleDark"
>
{{ isDark ? 'Dark' : 'Light' }}
</UButton>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
function toggleDark() {
colorMode.preference = isDark.value ? 'light' : 'dark'
}
</script>---
Built-in Color Mode Components
UColorModeSwitch
Simple toggle component:
<template>
<UColorModeSwitch />
</template>Renders a toggle switch that switches between light/dark modes.
UColorModeButton
Button component with icon:
<template>
<UColorModeButton />
</template>Renders a button that cycles through light/dark/system modes.
UColorModeSelect
Dropdown select component:
<template>
<UColorModeSelect />
</template>Renders a select menu with all three options (Light, Dark, System).
UColorModeImage
Display different images based on color mode:
<template>
<UColorModeImage
light="/logo-light.png"
dark="/logo-dark.png"
alt="Logo"
/>
</template>UColorModeAvatar
Display different avatars based on color mode:
<template>
<UColorModeAvatar
light="/avatar-light.jpg"
dark="/avatar-dark.jpg"
alt="Profile"
/>
</template>---
Custom Toggle Components
Icon-Only Toggle
<template>
<UButton
:icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
variant="ghost"
aria-label="Toggle color mode"
@click="toggleColorMode"
/>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
function toggleColorMode() {
colorMode.preference = isDark.value ? 'light' : 'dark'
}
</script>Toggle with Label
<template>
<div class="flex items-center gap-2">
<UIcon :name="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'" />
<USwitch v-model="isDarkMode" @update:model-value="toggleColorMode" />
<span class="text-sm">{{ isDark ? 'Dark' : 'Light' }} Mode</span>
</div>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
const isDarkMode = computed({
get: () => isDark.value,
set: (value) => {
colorMode.preference = value ? 'dark' : 'light'
}
})
</script>Three-Way Toggle
<template>
<UDropdownMenu>
<UButton :icon="currentIcon" variant="ghost">
{{ currentLabel }}
</UButton>
<template #content>
<UDropdownMenuItem
@click="colorMode.preference = 'light'"
:disabled="colorMode.preference === 'light'"
>
<UIcon name="i-heroicons-sun" class="mr-2" />
Light
</UDropdownMenuItem>
<UDropdownMenuItem
@click="colorMode.preference = 'dark'"
:disabled="colorMode.preference === 'dark'"
>
<UIcon name="i-heroicons-moon" class="mr-2" />
Dark
</UDropdownMenuItem>
<UDropdownMenuItem
@click="colorMode.preference = 'system'"
:disabled="colorMode.preference === 'system'"
>
<UIcon name="i-heroicons-computer-desktop" class="mr-2" />
System
</UDropdownMenuItem>
</template>
</UDropdownMenu>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const currentIcon = computed(() => {
switch (colorMode.preference) {
case 'light': return 'i-heroicons-sun'
case 'dark': return 'i-heroicons-moon'
case 'system': return 'i-heroicons-computer-desktop'
default: return 'i-heroicons-sun'
}
})
const currentLabel = computed(() => {
return colorMode.preference.charAt(0).toUpperCase() + colorMode.preference.slice(1)
})
</script>---
CSS Variables & Dark Mode
All Nuxt UI components automatically adapt to dark mode via CSS variables:
/* Automatically defined by Nuxt UI */
.dark {
--ui-bg: #1f2937;
--ui-text: #f9fafb;
--ui-primary: #3b82f6;
/* ... and many more */
}Custom Dark Mode Styles
Use the .dark class for custom dark mode styling:
<template>
<div class="
bg-white dark:bg-gray-900
text-gray-900 dark:text-white
border border-gray-200 dark:border-gray-700
">
Content adapts to color mode
</div>
</template>Accessing Color Mode in CSS
<style scoped>
.my-component {
background: white;
color: black;
}
.dark .my-component {
background: #1f2937;
color: white;
}
/* Or use Tailwind's dark: prefix */
</style>---
Advanced Configuration
Force Dark/Light Mode
Disable user preference and force a specific mode:
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
colorMode: {
preference: 'dark', // Force dark mode
fallback: 'dark' // Fallback if preference unavailable
}
}
})Disable Color Mode
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
colorMode: false // Disable color mode entirely
}
})Custom Storage Key
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
colorMode: {
storageKey: 'my-app-theme' // Custom localStorage key
}
}
})---
Theming Dark Mode Colors
Global Semantic Colors
Customize dark mode colors in app.config.ts:
// app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: 'blue',
success: 'green',
warning: 'amber',
error: 'red'
}
}
})These colors automatically adapt to dark mode with proper contrast.
Custom CSS Variables
Add custom CSS variables that adapt to color mode:
<style>
@theme {
/* Light mode (default) */
--custom-bg: #ffffff;
--custom-text: #000000;
}
.dark {
/* Dark mode overrides */
--custom-bg: #1f2937;
--custom-text: #f9fafb;
}
</style>---
Reactive to Color Mode Changes
Watch for Changes
<script setup lang="ts">
const colorMode = useColorMode()
watch(() => colorMode.value, (newMode) => {
console.log('Color mode changed to:', newMode)
// Perform actions on mode change
if (newMode === 'dark') {
// Dark mode specific logic
}
})
</script>One-time Setup
<script setup lang="ts">
const colorMode = useColorMode()
onMounted(() => {
if (colorMode.value === 'dark') {
// Initialize dark mode specific features
}
})
</script>---
Common Patterns
Header with Color Mode Toggle
<template>
<header class="border-b bg-white dark:bg-gray-900">
<UContainer>
<div class="flex items-center justify-between py-4">
<div class="flex items-center gap-2">
<UColorModeImage
light="/logo-light.svg"
dark="/logo-dark.svg"
alt="Logo"
class="h-8"
/>
<span class="font-bold text-xl">My App</span>
</div>
<nav class="flex items-center gap-4">
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<UColorModeButton />
</nav>
</div>
</UContainer>
</header>
</template>Settings Panel
<template>
<UCard>
<template #header>
<h3 class="font-semibold">Appearance</h3>
</template>
<div class="space-y-4">
<div>
<label class="text-sm font-medium">Theme</label>
<URadioGroup
v-model="colorMode.preference"
:items="themeOptions"
class="mt-2"
/>
</div>
<UDivider />
<div class="flex items-center justify-between">
<div>
<p class="font-medium">Dark Mode</p>
<p class="text-sm text-gray-500">Toggle dark mode on/off</p>
</div>
<UColorModeSwitch />
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const themeOptions = [
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
{ label: 'System', value: 'system' }
]
</script>---
Troubleshooting
Flash of Unstyled Content (FOUC)
Problem: Brief flash of light mode before dark mode applies
Solution: Color mode is applied before hydration by default. If you see FOUC:
1. Ensure @nuxt/ui is in modules 2. Verify UApp wrapper exists in app.vue 3. Check that CSS imports are in correct order
Preference Not Persisting
Problem: Color mode resets on page reload
Cause: localStorage access issues or browser restrictions
Solution:
// Check localStorage access
if (typeof localStorage !== 'undefined') {
console.log('localStorage available')
} else {
console.error('localStorage blocked')
}System Mode Not Working
Problem: System mode doesn't follow OS changes
Cause: Browser doesn't support prefers-color-scheme
Solution: Provide manual toggle as fallback:
<template>
<UColorModeSelect />
</template>---
Reference
- Templates: See
templates/components/ui-dark-mode-toggle.vue - Composables: See
composables-guide.mdfor useColorMode - Theming: See
component-theming-guide.mdfor color customization
Form Advanced Patterns
Multi-Step Forms
<script setup lang="ts">
const step = ref(1)
const formData = reactive({
step1: {},
step2: {},
step3: {}
})
function nextStep() {
if (step.value < 3) step.value++
}
function prevStep() {
if (step.value > 1) step.value--
}
</script>
<template>
<UForm v-if="step === 1" :state="formData.step1">
<!-- Step 1 fields -->
<UButton @click="nextStep">Next</UButton>
</UForm>
<UForm v-if="step === 2" :state="formData.step2">
<!-- Step 2 fields -->
<UButton @click="prevStep">Back</UButton>
<UButton @click="nextStep">Next</UButton>
</UForm>
<UForm v-if="step === 3" :state="formData.step3" @submit="onSubmit">
<!-- Step 3 fields -->
<UButton @click="prevStep">Back</UButton>
<UButton type="submit">Submit</UButton>
</UForm>
</template>File Uploads
<script setup lang="ts">
const files = ref<File[]>([])
async function handleUpload(event: Event) {
const target = event.target as HTMLInputElement
if (target.files) {
files.value = Array.from(target.files)
}
}
</script>
<template>
<UFormField label="Upload Files">
<input type="file" multiple @change="handleUpload" />
</UFormField>
</template>Dynamic Fields
<script setup lang="ts">
const items = ref([{ name: '', value: '' }])
function addItem() {
items.value.push({ name: '', value: '' })
}
function removeItem(index: number) {
items.value.splice(index, 1)
}
</script>
<template>
<div v-for="(item, index) in items" :key="index">
<UInput v-model="item.name" />
<UButton @click="removeItem(index)">Remove</UButton>
</div>
<UButton @click="addItem">Add Item</UButton>
</template>Loading & Feedback Patterns
Skeleton Loaders
Match dimensions of real content:
<div v-if="loading">
<USkeleton class="h-8 w-48 mb-4" /> <!-- Matches heading -->
<USkeleton class="h-4 w-full mb-2" /> <!-- Matches text -->
<USkeleton class="h-4 w-3/4" /> <!-- Matches text -->
</div>
<div v-else>
<h2>Real Heading</h2>
<p>Real content...</p>
</div>Progress Indicators
<script setup lang="ts">
const progress = ref(0)
async function upload() {
const interval = setInterval(() => {
progress.value += 10
if (progress.value >= 100) {
clearInterval(interval)
showSuccessToast()
}
}, 300)
}
</script>
<template>
<UProgress :value="progress" :max="100" />
</template>Toast Coordination
// Show loading toast
const loadingToast = addToast({
title: 'Processing...',
timeout: 0
})
// Do work
await doWork()
// Remove loading toast
removeToast(loadingToast.id)
// Show success toast
addToast({
title: 'Complete!',
color: 'success'
})Overlay Decision Guide
When to Use Which?
Modal
Use for: Full-featured overlays requiring user attention
- Forms that need focus
- Important dialogs
- Content requiring full attention
Desktop: Centered overlay Mobile: Consider using Drawer instead
Drawer
Use for: Side panels and mobile-first patterns
- Navigation menus
- Filter panels
- Mobile forms
- Settings panels
Positions: left, right, top, bottom
Dialog
Use for: Simple confirmations and alerts
- Yes/No confirmations
- Delete confirmations
- Simple alerts
Simpler than Modal, focused on confirmation actions.
Popover
Use for: Contextual information and actions
- Rich tooltips
- Inline forms
- Dropdown menus
- Help text
Triggered: By click Positioning: Relative to trigger
Tooltip
Use for: Brief helper text
- Icon explanations
- Button descriptions
- Field hints
Triggered: By hover Keep brief: 1-2 lines max
Sheet
Use for: Bottom sheets (mobile pattern)
- Mobile action sheets
- Mobile selections
- Swipeable panels
Responsive Pattern
<!-- Desktop: Modal -->
<UModal v-if="!isMobile" v-model="isOpen">
...
</UModal>
<!-- Mobile: Drawer -->
<UDrawer v-else v-model="isOpen" side="bottom">
...
</UDrawer>Responsive Patterns
Media Query Detection
const isMobile = computed(() => {
if (process.client) {
return window.matchMedia('(max-width: 768px)').matches
}
return false
})Responsive Components
Modal ↔ Drawer
<UModal v-if="!isMobile" v-model="isOpen">...</UModal>
<UDrawer v-else v-model="isOpen" side="bottom">...</UDrawer>Grid Layouts
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<UCard v-for="item in items">...</UCard>
</div>Breakpoints
- sm: 640px
- md: 768px
- lg: 1024px
- xl: 1280px
- 2xl: 1536px
Mobile-First Utilities
Always design mobile-first:
<!-- Mobile: Stack | Desktop: Row -->
<div class="flex flex-col md:flex-row gap-4">
...
</div><template>
<!-- UApp wrapper is REQUIRED for Nuxt UI components to work -->
<UApp>
<!-- Render pages -->
<NuxtPage />
<!-- Toast notifications container (auto-managed by useToast) -->
<!-- Notifications container (auto-managed by useNotification) -->
</UApp>
</template>
<style>
/* Import Tailwind CSS - MUST be first */
@import "tailwindcss";
/* Import Nuxt UI - MUST be after Tailwind */
@import "@nuxt/ui";
/* Optional: Custom global styles */
body {
@apply bg-default text-default;
}
/* Optional: Custom CSS variables for theming */
:root {
/* Add custom CSS variables if needed */
}
</style>
<script setup lang="ts">
// Optional: Global setup, meta tags, etc.
useHead({
titleTemplate: '%s - Nuxt UI v4 App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
})
// Optional: Global keyboard shortcuts
defineShortcuts({
// Example: Open command palette with Cmd+K
// 'meta_k': () => openCommandPalette()
})
// Optional: Color mode preference
// const colorMode = useColorMode()
// colorMode.preference = 'system' // or 'light' or 'dark'
</script>
Related skills
How it compares
Choose nuxt-ui-v4 for opinionated Nuxt dashboards and AI chat UIs; choose a headless component skill when you need unstyled primitives without Nuxt UI conventions.
FAQ
How many components does nuxt-ui-v4 cover?
nuxt-ui-v4 documents 125+ Nuxt UI v4.6+ components including 10 dashboard, 8 chat/AI, 6 editor, 16 page-layout, 22 form, and 8 overlay components for Nuxt v4 and plain Vue apps via the Vite plugin.
Does nuxt-ui-v4 work with AI SDK v5 chat?
Yes. nuxt-ui-v4 integrates AI SDK v5 using the Chat class with UChatMessages, UChatPrompt, UChatReasoning, and UChatTool components plus isReasoningStreaming and isToolStreaming helpers from @nuxt/ui/utils/ai.