
Nuxt Ui
- 15.6k installs
- 6.8k repo stars
- Updated July 30, 2026
- nuxt/ui
nuxt-ui is an agent skill that Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, buildi.
About
Build UIs with nuxt ui v4 125 accessible Vue components with Tailwind CSS theming Use when creating interfaces customizing themes to match a brand building forms or composing layouts like dashboards docs sites and chat interfaces name nuxt-ui description Build UIs with nuxt ui v4 125 accessible Vue components with Tailwind CSS theming Use when creating interfaces customizing themes to match a brand building forms or composing layouts like dashboards docs sites and chat interfaces Nuxt UI Vue component library built on Reka UI https reka-ui com Tailwind CSS https tailwindcss com Tailwind Variants https www tailwind-variants org Works with Nuxt Vue Vite Laravel Vite Inertia and AdonisJS Vite Inertia MCP Server For component API details props slots events full documentation examples use the Nuxt UI MCP server https ui nuxt com docs getting-started ai mcp If not already configured add it Cursor cursor mcp json json mcpServers nuxt-ui type http url https ui nuxt com mcp Claude Code bash claude mcp add transport http nuxt-ui https ui nuxt com
- `search_components` - find components by name, description, or category (no params = list all)
- `search_composables` - find composables by name or description (no params = list all)
- `search_icons` - search Iconify icons (defaults to `lucide`), returns `i-{prefix}-{name}` names
- `get_component` - full component documentation with usage examples
- `get_component_metadata` - props, slots, events (lightweight, no docs content)
Nuxt Ui by the numbers
- 15,643 all-time installs (skills.sh)
- +335 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #31 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
nuxt-ui capabilities & compatibility
- Capabilities
- `search_components` — find components by name, d · `search_composables` — find composables by name · `search_icons` — search iconify icons (defaults · `get_component` — full component documentation w · `get_component_metadata` — props, slots, events
- Use cases
- documentation
What nuxt-ui says it does
--- name: nuxt-ui description: Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming.
Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites, and chat interfaces.
--- # Nuxt UI Vue component library built on [Reka UI](https://reka-ui.com/) + [Tailwind CSS](https://tailwindcss.com/) + [Tailwind Variants](https://www.tailwind-variants.org/).
Works with Nuxt, Vue (Vite), Laravel (Vite + Inertia), and AdonisJS (Vite + Inertia).
npx skills add https://github.com/nuxt/ui --skill nuxt-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15.6k |
|---|---|
| repo stars | ★ 6.8k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | nuxt/ui ↗ |
What problem does nuxt-ui solve for developers using this skill?
Build UIs with @nuxt/ui v4 - 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like d.
Who is it for?
Developers who need nuxt-ui patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dash
What you get
Actionable workflows and conventions from SKILL.md for nuxt-ui.
- themed Vue UI layouts
- form and dashboard compositions
- component configurations
By the numbers
- 125+ accessible Vue components in @nuxt/ui v4
- Targets @nuxt/ui v4 per skill manifest
Files
Nuxt UI
Vue component library built on Reka UI + Tailwind CSS + Tailwind Variants. Works with Nuxt, Vue (Vite), Laravel (Vite + Inertia), and AdonisJS (Vite + Inertia).
MCP Server
For component API details (props, slots, events, full documentation, examples), use the Nuxt UI MCP server. If not already configured, add it:
Cursor — .cursor/mcp.json:
{ "mcpServers": { "nuxt-ui": { "type": "http", "url": "https://ui.nuxt.com/mcp" } } }Claude Code:
claude mcp add --transport http nuxt-ui https://ui.nuxt.com/mcpKey MCP tools:
search_components— find components by name, description, or category (no params = list all)search_composables— find composables by name or description (no params = list all)search_icons— search Iconify icons (defaults tolucide), returnsi-{prefix}-{name}namesget_component— full component documentation with usage examplesget_component_metadata— props, slots, events (lightweight, no docs content)get_example— real-world code examples
When you need to know what a component accepts or how its API works, use the MCP. This skill teaches you when to use which component and how to build well.
Core rules (always apply)
1. Always wrap the app in `UApp` — required for toasts, tooltips, and programmatic overlays. Accepts a locale prop for i18n. 2. Always use semantic colors — text-default, bg-elevated, border-muted, etc. Never use raw Tailwind palette colors like text-gray-500. 3. Read generated theme files for slot names — Nuxt: .nuxt/ui/<component>.ts, Vue: node_modules/.nuxt-ui/ui/<component>.ts. These show every slot, variant, and default class for any component. 4. Override priority (highest wins): ui prop / class prop → global config → theme defaults. 5. Icons use `i-{collection}-{name}` format — lucide is the default collection. Use the MCP search_icons tool to find icons, or browse at icones.js.org.
How to use this skill
Based on the task, load the relevant reference files before writing any code. Don't load everything — only what's needed.
Reference files
Guidelines — design decisions and conventions:
- design-system — semantic colors, theming, brand customization, variants, the
uiprop - component-selection — decision matrices: when to use Modal vs Slideover, Select vs SelectMenu, Toast vs Alert, etc.
- conventions — coding patterns, slot naming, items arrays, composables, keyboard shortcuts
- forms — form validation, field layout, error handling, Standard Schema
Layouts — full page structure patterns:
- landing — landing pages, blog, changelog, pricing
- dashboard — admin UI with sidebar and panels
- docs — documentation sites with navigation and TOC
- chat — AI chat with Vercel AI SDK
- editor — rich text editor with toolbars
Recipes — complete patterns for common tasks:
- data-tables — tables with filters, pagination, sorting, selection
- auth — login, signup, forgot password forms
- overlays — modals, slideovers, drawers, command palette
- navigation — headers, sidebars, breadcrumbs, tabs
Quick reference:
- components — categorized component index for finding the right component name
Routing table
| Task | Load these references |
|---|---|
| Build a landing page | design-system, conventions, landing |
| Build a dashboard / admin UI | conventions, component-selection, dashboard |
| Add a settings page | conventions, forms |
| Create a login / signup form | conventions, forms, auth |
| Display data in a table | conventions, component-selection, data-tables |
| Customize theme / brand colors | design-system |
| Add a chat interface | conventions, chat |
| Add a modal, slideover, or drawer | conventions, component-selection, overlays |
| Build site navigation | conventions, component-selection, navigation |
| Build a documentation site | conventions, docs |
| Render markdown | component-selection, components, docs |
| Add a rich text editor | conventions, editor |
| General UI work | conventions, component-selection |
Installation
Nuxt
pnpm add @nuxt/ui tailwindcss// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";<!-- app.vue -->
<template>
<UApp>
<NuxtPage />
</UApp>
</template>Vue (Vite)
pnpm add @nuxt/ui tailwindcss// 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()
]
})// src/main.ts
import './assets/css/main.css'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
routes: [],
history: createWebHistory()
})
app.use(router)
app.use(ui)
app.mount('#app')/* src/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";<!-- src/App.vue -->
<template>
<UApp>
<RouterView />
</UApp>
</template>Addclass="isolate"to your root<div id="app">inindex.html.
For Inertia: useui({ router: 'inertia' })invite.config.ts.
Components
Quick-reference index of all 125+ components. For full API docs (props, slots, events, examples), use the MCP get_component or get_component_metadata tools.
Layout
| Component | Purpose |
|---|---|
UApp | Required root wrapper — toasts, tooltips, overlays, i18n |
UHeader | Responsive header with mobile menu |
UFooter | Footer with left/right/top/bottom slots |
UFooterColumns | Multi-column footer with link groups |
UMain | Main content area |
UContainer | Centered max-width container |
ULink | Enhanced link — NuxtLink/RouterLink with active states |
Element
| Component | Purpose |
|---|---|
UButton | Buttons — links, actions, icons, loading states |
UBadge | Labels, tags, status indicators |
UAvatar | User photos, initials, icons |
UAvatarGroup | Stacked avatars with max limit |
UIcon | Iconify icons (i-{collection}-{name}) |
UCard | Bordered container with header/body/footer |
UAlert | Inline messages — info, warning, error, success |
UBanner | App-wide sticky announcement bar |
UChip | Notification dot overlay on children |
UKbd | Keyboard key display |
USeparator | Divider line with optional label |
USkeleton | Loading placeholder |
UProgress | Progress bar |
UToast | Toast notification (shown via useToast) |
UCalendar | Date calendar (single, range, multiple) |
UCollapsible | Animated expand/collapse |
UFieldGroup | Group form inputs horizontally |
UMarquee | Scrolling content ticker |
UCarousel | Image/content carousel with autoplay |
UEmpty | Empty state placeholder with icon, title, actions |
UError | Error display with retry action |
UScrollArea | Scrollable area with custom scrollbar |
UTimeline | Timeline display for events and activity |
UUser | User display — avatar + name + description |
UTheme | Theme provider — scoped color overrides for children |
Form
| Component | Purpose |
|---|---|
UAuthForm | Pre-built auth form with social providers |
UInput | Text input — text, email, password, search |
UTextarea | Multi-line text with autoresize |
USelect | Native-like dropdown for small lists |
USelectMenu | Rich searchable dropdown, multi-select, groups |
UInputMenu | Autocomplete / combobox |
UInputNumber | Numeric input with +/- controls |
UInputDate | Date picker with calendar |
UInputTime | Time picker (12/24h) |
UInputTags | Tag/chip input |
UPinInput | Verification code input |
UCheckbox | Single boolean checkbox |
UCheckboxGroup | Multiple checkboxes |
URadioGroup | Radio button group |
USwitch | Toggle switch |
USlider | Range slider |
UColorPicker | Color picker (hex/rgb/hsl) |
UFileUpload | File upload (button or drop area) |
UForm | Validation wrapper with Standard Schema |
UFormField | Field wrapper with label, hint, errors |
Overlay
| Component | Purpose |
|---|---|
UModal | Centered dialog — confirmations, forms |
USlideover | Side panel — details, editing |
UDrawer | Bottom sheet — mobile actions |
UPopover | Contextual popup attached to trigger |
UTooltip | Hover/focus hint (non-interactive) |
UContextMenu | Right-click menu |
UCommandPalette | Search + keyboard navigation (Cmd+K) |
Navigation
| Component | Purpose |
|---|---|
USidebar | Standalone sidebar with header/body/footer |
UNavigationMenu | Primary nav — horizontal or vertical |
UTabs | Tab switcher within a page |
UBreadcrumb | Location hierarchy |
UDropdownMenu | Action menu on a trigger |
UPagination | Page navigation |
UStepper | Multi-step wizard |
UAccordion | Collapsible sections |
Data
| Component | Purpose |
|---|---|
UTable | Data table (TanStack Table) with sorting, selection, pinning |
UTree | Hierarchical tree view |
Dashboard
| Component | Purpose |
|---|---|
UDashboardGroup | Root dashboard wrapper |
UDashboardSidebar | Resizable, collapsible sidebar |
UDashboardPanel | Content panel with header/body/footer |
UDashboardNavbar | Panel header bar |
UDashboardToolbar | Filter/action bar below navbar |
UDashboardResizeHandle | Resize handle between panels |
UDashboardSidebarToggle | Mobile sidebar toggle button |
UDashboardSearchButton | Search button for sidebar |
UDashboardSearch | Dashboard-level search overlay |
UDashboardSidebarCollapse | Collapse button for sidebar |
Page (marketing)
| Component | Purpose |
|---|---|
UPage | Multi-column layout with left/right sidebars |
UPageHero | Hero section — title, description, links, media |
UPageSection | Content section with features grid |
UPageCTA | Call to action block |
UPageHeader | Page title and description |
UPageBody | Main content area |
UPageGrid | Card grid layout |
UPageColumns | Multi-column layout |
UPageCard | Content card for grids |
UPageFeature | Feature item |
UPageLogos | Logo cloud |
UPageAside | Sticky sidebar wrapper |
UPageAnchors | Simple anchor links |
UPageLinks | Related resource links |
UPageList | List layout for page items |
Blog & Changelog
| Component | Purpose |
|---|---|
UBlogPosts | Blog post grid |
UBlogPost | Individual post card |
UChangelogVersions | Changelog list |
UChangelogVersion | Individual changelog entry |
Pricing
| Component | Purpose |
|---|---|
UPricingPlans | Pricing plan cards |
UPricingPlan | Individual pricing plan card |
UPricingTable | Feature comparison table |
Prose — Base Typography
Standard Markdown elements auto-resolved by Comark/Content/MDC. No :: prefix needed — they map directly from markdown syntax (# Heading → ProseH1, **bold** → ProseStrong, etc.). Themed via appConfig.ui.prose.<name>.
| Component | Renders | Notable |
|---|---|---|
H1 H2 H3 H4 | Headings | H1–H3 get anchor links + TOC entries |
P | Paragraph | |
A | Link | External links get target/rel handling |
Strong | Bold | |
Em | Italic | |
Blockquote | Blockquote | |
Hr | Horizontal rule | |
Ul Ol Li | Lists | Supports nesting and mixed lists |
Table Thead Tbody Tr Th Td | Tables | |
Img | Image | Zoom on click (:zoom="false" to disable), @nuxt/image support |
Pre | Code block | Copy button, filename + icon, line highlighting ({2,4-6}), diff |
Code | Inline code | color and lang props |
Prose — Feature Components
Nuxt UI-specific Prose components. In markdown files they are used without the `Prose` prefix (e.g. ::callout, ::steps). In Vue they are referenced as ProseCallout, ProseSteps, etc. Comark resolves them automatically when @nuxt/ui is installed.
Nuxt UI also registers shorthand aliases for Callout: ::note, ::tip, ::warning, ::caution (preset color + icon).
| Component | Purpose |
|---|---|
Callout | Highlighted note/warning/tip (color, icon, to) |
Badge | Inline badge/tag |
Kbd | Keyboard key |
Icon | Inline Iconify icon |
Prompt | Terminal prompt block |
Card CardGroup | Content card and card grid |
Steps | Numbered step list (level prop sets heading depth) |
Tabs TabsItem | Tabbed content (sync for localStorage, hash for scroll-on-change) |
Accordion AccordionItem | Collapsible accordion sections |
Collapsible | Single collapsible section |
Field FieldGroup | Form field display |
CodeGroup | Tabbed code blocks |
CodeCollapse | Collapsible code block |
CodeIcon | File-type icon in code headers |
CodePreview | Code + live rendered preview side by side |
CodeTree | File tree display |
Script | Script injection |
Content (Nuxt Content)
| Component | Purpose |
|---|---|
UContentNavigation | Sidebar navigation from content |
UContentToc | Table of contents |
UContentSurround | Prev/next navigation |
UContentSearch | Search command palette |
UContentSearchButton | Trigger for content search |
Chat (AI)
| Component | Purpose |
|---|---|
UChatMessages | Scrollable message list |
UChatMessage | Individual message bubble |
UChatReasoning | Collapsible AI reasoning block |
UChatTool | Tool invocation status |
UChatShimmer | Streaming text animation |
UChatPrompt | Enhanced textarea for prompts |
UChatPromptSubmit | Submit button with status |
UChatPalette | Chat layout for overlays |
Editor
| Component | Purpose |
|---|---|
UEditor | Rich text editor (JSON/HTML/Markdown) |
UEditorToolbar | Toolbar (fixed/bubble/floating) |
UEditorDragHandle | Block drag-and-drop |
UEditorSuggestionMenu | Slash command menu |
UEditorMentionMenu | @ mention menu |
UEditorEmojiMenu | Emoji picker |
Color Mode
| Component | Purpose |
|---|---|
UColorModeButton | Toggle button (light/dark) |
UColorModeSwitch | Toggle switch (light/dark) |
UColorModeSelect | Dropdown (light/dark/system) |
UColorModeAvatar | Avatar that changes with color mode |
UColorModeImage | Image that changes with color mode |
Component Selection
Decision matrices for choosing the right component. When in doubt, use the MCP search_components tool.
Overlays
| Need | Component | Why |
|---|---|---|
| Confirmation dialog, focused task, form | UModal | Blocks page interaction, centered, draws focus |
| Detail panel, settings, secondary content | USlideover | Slides from edge, doesn't feel as interruptive as modal |
| Mobile-first bottom sheet | UDrawer | Natural mobile pattern, swipe to dismiss |
| Contextual info attached to a trigger | UPopover | No backdrop, positioned relative to trigger |
| Simple hover hint | UTooltip | Non-interactive, hover/focus only |
Rules
- Use
UModalfor destructive confirmations ("Are you sure you want to delete?") - Use
USlideoverfor detail views in dashboards (email preview, user profile) - Use
UDrawerfor mobile navigation or action sheets - Modal and Slideover support
mode="drawer"for automatic mobile drawer behavior - For programmatic overlays, use
useOverlay()instead ofv-model:open - Never put interactive content (buttons, links) inside
UTooltip
Navigation
| Need | Component | Why |
|---|---|---|
| Primary site/app navigation | UNavigationMenu | Horizontal (header) or vertical (sidebar) |
| Switch between views on same page | UTabs | Content stays on page, no route change needed |
| Show current location in hierarchy | UBreadcrumb | Nested page structures |
| Search + keyboard-driven navigation | UCommandPalette | Power users, global search |
| Contextual actions on a trigger element | UDropdownMenu | Right-click menus, action buttons |
| Step-by-step process | UStepper | Multi-step forms, wizards |
Rules
- Use
UNavigationMenuwithorientation="vertical"in sidebars, default horizontal in headers - Use
UTabswhen switching views that don't need their own URL - Use route-based navigation (
toprop) when views should have shareable URLs UCommandPaletteis typically opened viaCmd+Kshortcut — usedefineShortcutsto wire it up
Inputs
| Need | Component | Why |
|---|---|---|
| Small fixed list (< 10 items) | USelect | Native-like, simple, lightweight |
| Searchable list, multiple selection, groups | USelectMenu | Rich dropdown with search, multi-select, grouped items |
| Autocomplete / combobox (type + select) | UInputMenu | User can type freely AND pick from suggestions |
| Free text entry | UInput | Plain text, email, password, search |
| Multi-line text | UTextarea | With autoresize and maxrows |
| Numeric value with +/- controls | UInputNumber | Min/max/step constraints |
| Date selection | UInputDate | Calendar dropdown, supports ranges |
| Time selection | UInputTime | Hour/minute picker, 12/24 hour |
| Tags / multi-value free text | UInputTags | Chip-style input with max limit |
| Verification code | UPinInput | Fixed-length code entry |
| Boolean toggle | USwitch | On/off, enable/disable |
| Boolean checkbox | UCheckbox | Single option with label |
| Multiple choices from a list | UCheckboxGroup | Multiple selection, vertical or horizontal |
| Single choice from a list (visible) | URadioGroup | All options visible, one selected |
| Range value | USlider | Min/max with visual track |
| Color value | UColorPicker | Hex/RGB/HSL picker |
| File upload | UFileUpload | Button or drop area variants |
Rules
- Use
UAuthFormfor login/signup pages — handles fields, social providers, validation, and layout out of the box - Use
USelectfor short, known lists (country, status, role) - Use
USelectMenuwhen the list is long or needs search - Use
UInputMenuwhen the user might want to type a value that's not in the list - Wrap all form inputs in
UFormFieldfor labels, descriptions, hints, and validation errors - Group related inline inputs with
UFieldGroup
Feedback
| Need | Component | Why |
|---|---|---|
| Ephemeral notification after action | useToast() | Auto-dismisses, stacks, non-blocking |
| Inline persistent message | UAlert | Stays visible, in-page context |
| App-wide announcement | UBanner | Sticky top bar, dismissible |
| Loading state | USkeleton | Placeholder shimmer while loading |
| Progress indicator | UProgress | Determinate or indeterminate progress |
Rules
- Use
useToast()for action feedback: "Item saved", "Email sent", "Error occurred" - Use
UAlertfor contextual warnings in forms or sections - Use
UBannerfor site-wide messages (maintenance, new feature) - Never use a toast for information the user needs to act on — use an alert or modal instead
Markdown
When rendering Markdown (for instance with Comark), prefer Prose components — they are styled and tuned for Markdown contexts. Generic Nuxt UI components can also be used. <ComarkRenderer> (or <Comark>) auto-resolves ProseX components when @nuxt/ui is installed. In Markdown, the Prose prefix can be omitted (::callout, ::steps, etc.).
| Need | Use | Not |
|---|---|---|
| Note / warning / tip | Callout | UAlert |
| Tabbed content | Tabs + TabsItem | UTabs |
| Step-by-step list | Steps | custom list |
| Content card grid | Card + CardGroup | UCard |
| Collapsible section | Collapsible / Accordion | UCollapsible / UAccordion |
| Tabbed code blocks | CodeGroup | manual tabs |
Rules
- Prose components use native Vue slots — Comark maps named
#slotblocks directly to<slot name="..." /> - Theme via
appConfig.ui.prose.<name>using the same override pattern as other Nuxt UI components Calloutcolors:neutral(default),primary,secondary,info,success,warning,error
Layout containers
| Need | Component | Why |
|---|---|---|
| Grouped content with header/body/footer | UCard | Bordered/shadow container with slots |
| Rich content card with icon, badge, links | UPageCard | Extended card for grids — supports icon, badge, highlight, links |
| Marketing page section | UPageSection | Full-width section with headline, title, features |
| Page hero | UPageHero | Title + description + links + optional media |
| Call to action | UPageCTA | Highlighted section with action links |
| Feature grid | UPageGrid + UPageCard | Multi-column card grid |
| Centered content wrapper | UContainer | Max-width container |
| Collapsible section | UCollapsible | Animated expand/collapse |
| Accordion (multiple collapsibles) | UAccordion | FAQ, grouped collapsible content |
Rules
- Don't overuse
UCard— plain content with spacing is often better than wrapping everything in cards - Use
UPageCardinstead ofUCardwhen you need icon, badge, highlight, or links — it's designed for feature grids and landing pages - Use
UPageSectionfor marketing/landing page sections, not for app UI - Use
UContainerinsideUDashboardPanelbody for consistent content width
Conventions
Coding patterns specific to Nuxt UI.
Auto-registered modules
Nuxt UI automatically registers @nuxt/icon, @nuxt/fonts, and @nuxtjs/color-mode. Do not add them to your modules array. Configure them via root-level keys in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
icon: { /* @nuxt/icon options */ },
fonts: { /* @nuxt/fonts options */ },
colorMode: { /* @nuxtjs/color-mode options */ }
})Disable any of them: ui: { fonts: false }, ui: { colorMode: false }.
Content module integration
When using @nuxt/content, it must come after @nuxt/ui in the modules array — otherwise prose components won't be available:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxt/content']
})Add @source in your CSS so Tailwind generates classes used in markdown/MDC:
/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
@source "../../../content/**/*";Use mapContentNavigation to transform content navigation for components like UBreadcrumb:
import { mapContentNavigation } from '@nuxt/ui/utils/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'
const breadcrumb = computed(() =>
mapContentNavigation(findPageBreadcrumb(navigation.value, page.value?.path))
)IDE setup
Recommended .vscode/settings.json for Tailwind IntelliSense autocomplete with Nuxt UI:
{
"files.associations": { "*.css": "tailwindcss" },
"editor.quickSuggestions": { "strings": "on" },
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.classFunctions": ["defineAppConfig"]
}UApp wrapper
Always wrap your app in UApp — it provides:
- Toast container (
useToast) - Tooltip provider
- Programmatic overlay context (
useOverlay) - i18n locale support
<UApp :locale="fr">
<NuxtPage /> <!-- or <RouterView /> for Vue -->
</UApp>Icons
Nuxt UI registers @nuxt/icon automatically. Format: i-{collection}-{name}. Prefer lucide collection.
<UIcon name="i-lucide-sun" class="size-5" />
<UButton icon="i-lucide-plus" label="Add" />
<UAlert icon="i-lucide-info" title="Heads up" />Install collections locally for reliable SSR and best performance:
pnpm i @iconify-json/lucide
pnpm i @iconify-json/simple-iconsCustom local collections (Nuxt only):
// nuxt.config.ts
export default defineNuxtConfig({
icon: {
customCollections: [{
prefix: 'custom',
dir: './app/assets/icons'
}]
}
})Default icon overrides
Components like Modal, Select, Accordion, etc. use default icons from appConfig.ui.icons. Override them globally:
// app.config.ts
export default defineAppConfig({
ui: {
icons: {
loading: 'i-lucide-refresh-cw',
close: 'i-lucide-x',
check: 'i-lucide-check',
chevronDown: 'i-lucide-chevron-down',
chevronRight: 'i-lucide-chevron-right',
arrowLeft: 'i-lucide-arrow-left',
arrowRight: 'i-lucide-arrow-right'
}
}
})Slot patterns
Most components follow consistent slot naming:
| Slot | Used by | Purpose |
|---|---|---|
#header | Card, Modal, Slideover, DashboardPanel | Top section |
#body | DashboardPanel | Scrollable content area |
#footer | Card, Modal, Slideover, DashboardPanel | Bottom section |
#left | Page, DashboardNavbar | Left sidebar or content |
#right | Page, DashboardNavbar, Header | Right sidebar or content |
#leading | Input, Button, Alert | Before main content (icon area) |
#trailing | Input, Button | After main content (icon area) |
#content | Modal, Slideover, Popover, Tooltip | Full content override |
#default | Most components | Main content area |
Items arrays
Many components accept an items prop. Two patterns:
Flat array — plain list:
const items = [
{ label: 'Edit', icon: 'i-lucide-pencil' },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
]Nested array — groups with automatic separators between them:
const items = [
[
{ label: 'Edit', icon: 'i-lucide-pencil' },
{ label: 'Duplicate', icon: 'i-lucide-copy' }
],
[
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
]
]Components supporting nested arrays: UDropdownMenu, UContextMenu, UCommandPalette, UNavigationMenu.
Composables
useToast
const toast = useToast()
toast.add({
title: 'Success',
description: 'Item saved',
color: 'success',
icon: 'i-lucide-check-circle',
duration: 5000,
actions: [{ label: 'Undo', onClick: () => {} }]
})
toast.remove('toast-id')
toast.clear()useOverlay
Programmatic modals, slideovers, drawers — no template v-model needed. See overlays recipe for full patterns.
const overlay = useOverlay()
const modal = overlay.create(MyComponent)
const instance = modal.open({ title: 'Confirm?' })
if (await instance.result) { /* confirmed */ }defineShortcuts
defineShortcuts({
meta_k: () => openSearch(),
escape: () => close(),
meta_enter: {
handler: () => submit(),
whenever: [isFormValid]
}
})Keys: meta (Cmd/Ctrl), ctrl, alt, shift. Separator: _.
extractShortcuts
Wire up keyboard shortcuts from menu items:
const items = [
{ label: 'New file', kbds: ['meta', 'n'], onSelect: () => newFile() },
{ label: 'Save', kbds: ['meta', 's'], onSelect: () => save() }
]
defineShortcuts(extractShortcuts(items))Internationalization (i18n)
Nuxt UI supports 50+ locales. Set the locale on UApp — all components inherit it.
Static locale
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<NuxtPage />
</UApp>
</template>Extend a built-in locale
extendLocale is auto-imported. Override specific messages or the code (affects date/time formatting in Calendar, InputDate, InputTime):
import { en } from '@nuxt/ui/locale'
const locale = extendLocale(en, {
code: 'en-AU',
messages: {
commandPalette: { placeholder: 'Search a component...' }
}
})Custom locale from scratch
import type { Messages } from '@nuxt/ui'
const locale = defineLocale<Messages>({
name: 'My locale',
code: 'en',
dir: 'ltr',
messages: {
// all component message keys
}
})Dynamic locale with @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', name: 'English' },
{ code: 'fr', name: 'Français' },
{ code: 'ar', name: 'العربية' }
]
}
})<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
const lang = computed(() => locales[locale.value]?.code)
const dir = computed(() => locales[locale.value]?.dir)
useHead({
htmlAttrs: { lang, dir }
})
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>Each locale has a dir property ('ltr' or 'rtl'). UApp uses it to set directionality on all components. Use useHead to propagate lang and dir to the <html> element.
Color mode
Nuxt UI registers @nuxtjs/color-mode automatically. Built-in components for switching:
UColorModeButton— single button toggle (light/dark)UColorModeSwitch— toggle switchUColorModeSelect— dropdown with system/light/dark optionsUColorModeAvatar— displays different avatar per modeUColorModeImage— displays different image per mode
For custom color mode UI, use useColorMode with ClientOnly to avoid hydration mismatch:
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed({
get: () => colorMode.value === 'dark',
set: (v) => { colorMode.preference = v ? 'dark' : 'light' }
})
</script>
<template>
<ClientOnly>
<USwitch v-model="isDark" />
<template #fallback>
<div class="size-8" />
</template>
</ClientOnly>
</template>Official templates
Bootstrap a project from a template instead of starting from scratch:
npx nuxi@latest init -t ui # Starter
npx nuxi@latest init -t ui/dashboard # Dashboard
npx nuxi@latest init -t ui/docs # Docs (Nuxt Content)
npx nuxi@latest init -t ui/landing # Landing page
npx nuxi@latest init -t ui/saas # SaaS (landing + pricing + docs + blog)
npx nuxi@latest init -t ui/chat # AI chat (Vercel AI SDK)
npx nuxi@latest init -t ui/editor # Rich text editor
npx nuxi@latest init -t ui/portfolio # Portfolio
npx nuxi@latest init -t ui/changelog # ChangelogResponsive patterns
- Dashboard sidebar hides on mobile, shows a slideover/drawer via
UDashboardSidebarmodeprop UHeaderbody slot is the mobile menu content (shown when hamburger is tapped)- Most components handle responsiveness automatically — avoid manual breakpoint classes unless needed
- Use
UPageAsidefor sidebars that should hide belowlgbreakpoint
Design System
Semantic colors
Nuxt UI uses 7 semantic colors. Never use raw Tailwind palette colors in components — always use these semantic names.
| Color | Default | When to use |
|---|---|---|
primary | green | CTAs, active states, brand accent, links |
secondary | blue | Secondary actions, complementary highlights |
success | green | Success messages, confirmations, positive states |
info | blue | Informational alerts, tips, neutral highlights |
warning | yellow | Warnings, caution states, pending actions |
error | red | Errors, destructive actions, validation failures |
neutral | slate | Text, borders, backgrounds, disabled states, chrome |
Choosing colors for components
- Primary action on a page (submit, save, confirm) →
color="primary" - Secondary actions (cancel, back, alternative) →
color="neutral"withvariant="outline"or"ghost" - Destructive actions (delete, remove) →
color="error" - Status indicators → match the semantic meaning:
success,warning,error,info - Navigation and chrome →
color="neutral"
Configuring colors
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: 'indigo',
secondary: 'violet',
success: 'emerald',
error: 'rose',
neutral: 'zinc'
}
}
})// Vue — vite.config.ts
ui({
ui: {
colors: { primary: 'indigo', secondary: 'violet', neutral: 'zinc' }
}
})Only colors that exist in your theme work — either Tailwind's defaults or custom colors defined with @theme.
Available color palettes:
- Standard Tailwind: red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
- Neutral palettes (for
neutralkey — pick one that matches the aesthetic): slate— cool blue-gray, professional (default)gray— true neutral, cleanzinc— slightly cool, modern, techyneutral— perfectly balancedstone— warm gray, earthytaupe— warm brown-gray, sophisticatedmauve— purple-tinted gray, elegantmist— soft blue-gray, airyolive— green-tinted gray, natural
Adding custom brand colors
1. Define all 11 shades in CSS:
/* app/assets/css/main.css */
@theme static {
--color-brand-50: #fef2f2;
--color-brand-100: #fee2e2;
--color-brand-200: #fecaca;
--color-brand-300: #fca5a5;
--color-brand-400: #f87171;
--color-brand-500: #ef4444;
--color-brand-600: #dc2626;
--color-brand-700: #b91c1c;
--color-brand-800: #991b1b;
--color-brand-900: #7f1d1d;
--color-brand-950: #450a0a;
}2. Assign it: ui: { colors: { primary: 'brand' } }
Extending with new semantic color names
To add a color beyond the 7 defaults (e.g., tertiary), register it in theme.colors:
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
}
}
})Semantic utility classes
Use these everywhere instead of raw palette colors:
Text
text-default— primary body texttext-muted— secondary text (descriptions, hints)text-toned— medium-emphasis text (between muted and default)text-dimmed— tertiary text (placeholders, disabled)text-highlighted— emphasized text (headings, important labels)text-inverted— text on inverted backgrounds (pair withbg-inverted)
Backgrounds
bg-default— page backgroundbg-muted— subtle backgrounds (hover states, alternating rows)bg-elevated— raised surfaces (cards, dropdowns)bg-accented— accent backgrounds (active states, selected items)bg-inverted— inverse background (dark on light, light on dark)
Borders
border-default— standard bordersborder-muted— subtle borders (dividers, separators)border-accented— accent borders (active states)border-inverted— inverse borders
Variants
Most components accept a variant prop. Choose based on visual weight:
| Variant | Weight | When to use |
|---|---|---|
solid | Highest | Primary actions, main CTAs |
outline | Medium | Secondary actions, form fields |
soft | Medium-low | Tags, badges, subtle buttons |
subtle | Low | Background highlights, less prominent actions |
ghost | Lowest | Inline actions, icon buttons, navigation items |
link | Lowest | Text-only links inside content |
Rules
- One solid primary button per view — everything else should be lower weight
- Destructive buttons use
color="error"but not necessarilyvariant="solid"— usevariant="soft"or"outline"unless it's the primary action on a confirmation dialog - Button groups should use consistent variants — don't mix
solidandoutlinesiblings
Customizing components
ui prop
Override theme slots on a single instance — wins over global config and variants.
<UButton :ui="{ base: 'font-bold', trailingIcon: 'size-3 rotate-90' }" />
<UCard :ui="{ header: 'bg-muted', body: 'p-8' }" />Rules for ui overrides:
- Prefer `defaultVariants` over slot class overrides when possible (e.g., changing default button variant/size).
- Don't duplicate default classes — check the generated theme file first to see what's already there.
- Border radius defaults come from
--ui-radius, but you can override withrounded-*classes inuiorclasswhen you need a specific radius on a component.
class prop
Override the root (or base) slot only — simpler than ui for single-slot changes.
<UButton class="font-bold" />Finding slot names
Read the generated theme file for any component:
- Nuxt:
.nuxt/ui/<component>.ts - Vue:
node_modules/.nuxt-ui/ui/<component>.ts
These files show every available slot name, variant combination, and default class.
Global config
Override slots, variants, compoundVariants, and defaultVariants globally in app.config.ts (Nuxt) or vite.config.ts (Vue):
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
button: {
slots: {
base: 'font-bold'
},
compoundVariants: [{
color: 'neutral',
variant: 'outline',
class: 'ring-default hover:bg-accented'
}],
defaultVariants: {
color: 'neutral',
variant: 'outline'
}
}
}
})Tailwind Variants uses tailwind-merge under the hood — conflicting classes are resolved automatically.
UTheme (scoped overrides)
Override theme for a section of the component tree without affecting the rest of the app. Renders no DOM element — uses provide/inject:
<UTheme :ui="{ button: { slots: { base: 'rounded-full' } } }">
<UButton label="Rounded" />
<UButton label="Also rounded" />
</UTheme>Global defaultVariants
Override default size and color for all components at once:
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
defaultVariants: {
size: 'lg',
color: 'neutral'
}
}
}
})theme.transitions
Controls whether interactive components get transition-colors. Enabled by default.
// nuxt.config.ts — disable transitions
export default defineNuxtConfig({
ui: {
theme: {
transitions: false
}
}
})theme.prefix
When using Tailwind CSS with a prefix, configure the same prefix in Nuxt UI so component classes match:
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
prefix: 'tw'
}
}
})/* app/assets/css/main.css */
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";Tree-shaking with experimental.componentDetection
Enable automatic component detection to only generate CSS for components you actually use:
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
experimental: {
componentDetection: true
}
}
})For dynamic components (e.g., <component :is="...">), pass an array of component names to guarantee they're included:
componentDetection: ['Modal', 'Dropdown', 'Popover']CSS @theme customization
Customize Tailwind design tokens in main.css:
Fonts
@theme {
--font-sans: 'Public Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}In Nuxt, fonts defined here are automatically loaded by @nuxt/fonts.
Breakpoints
@theme {
--breakpoint-3xl: 1920px;
}CSS variables
Nuxt UI exposes CSS variables you can override in main.css:
:root {
--ui-radius: 0.25rem;
--ui-container: 80rem;
--ui-header-height: 4rem;
}Color shade overrides
Each semantic color defaults to shade 500 in light mode, 400 in dark mode. Override per-mode:
:root {
--ui-primary: var(--ui-color-primary-700);
}
.dark {
--ui-primary: var(--ui-color-primary-200);
}You can use var(--ui-color-<name>-<shade>) to reference shades from the active palette (e.g., var(--ui-color-neutral-800) maps to whichever neutral palette is configured).
Black/white as primary
black and white have no shades, so they can't be used in config. Set them directly:
:root {
--ui-primary: black;
}
.dark {
--ui-primary: white;
}Forms
Basic pattern
Nuxt UI forms use UForm + UFormField + Standard Schema validation (Zod, Valibot, Yup, or Joi).
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.email('Invalid email'),
password: z.string().min(8, 'Min 8 characters')
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ email: '', password: '' })
function onSubmit(event: FormSubmitEvent<Schema>) {
// UForm validates before emitting @submit — access validated data via event.data
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField name="email" label="Email" required>
<UInput v-model="state.email" type="email" placeholder="you@example.com" />
</UFormField>
<UFormField name="password" label="Password" required>
<UInput v-model="state.password" type="password" placeholder="Min 8 characters" />
</UFormField>
<UButton type="submit" label="Sign in" />
</UForm>
</template>Key rules
- Always use
UFormFieldaround inputs — it connects validation errors via thenameprop - The
nameprop onUFormFieldmust match the schema field name exactly - Use
reactive<Partial<Schema>>({})for state —Partialallows empty initial values @submitonly fires when validation passes- For nested objects, use dot notation:
name="address.city"
UFormField props
| Prop | Purpose |
|---|---|
name | Links to schema field for validation errors |
label | Visible label text |
description | Help text below the input |
hint | Right-aligned hint text (e.g., "Optional") |
required | Shows required indicator |
size | Inherits to child input |
Field layout patterns
Vertical stack (default)
<UForm :schema="schema" :state="state" class="space-y-4">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" />
</UFormField>
</UForm>Inline fields with UFieldGroup
<UFieldGroup>
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
</UFieldGroup>Grid layout
<UForm :schema="schema" :state="state" class="grid grid-cols-2 gap-4">
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
<UFormField name="email" label="Email" class="col-span-2">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>Common field patterns
Select
<UFormField name="role" label="Role">
<USelect v-model="state.role" :items="['Admin', 'Editor', 'Viewer']" placeholder="Choose role" />
</UFormField>Checkbox
<UFormField name="terms">
<UCheckbox v-model="state.terms" label="I agree to the terms and conditions" />
</UFormField>Radio group
<UFormField name="plan" label="Plan">
<URadioGroup
v-model="state.plan"
:items="[
{ label: 'Free', value: 'free', description: 'For personal projects' },
{ label: 'Pro', value: 'pro', description: 'For teams' }
]"
/>
</UFormField>Switch
<UFormField name="notifications" label="Email notifications">
<USwitch v-model="state.notifications" />
</UFormField>Textarea
<UFormField name="bio" label="Bio" description="Brief description for your profile.">
<UTextarea v-model="state.bio" :rows="3" autoresize :maxrows="6" />
</UFormField>File upload
<UFormField name="avatar" label="Avatar">
<UFileUpload v-model="state.avatar" accept="image/*" />
</UFormField>
<!-- Or as a drop area -->
<UFormField name="documents" label="Documents">
<UFileUpload v-model="state.documents" multiple variant="area" />
</UFormField>Date
<UFormField name="date" label="Date">
<UInputDate v-model="state.date" />
</UFormField>
<!-- Date range -->
<UFormField name="dateRange" label="Date range">
<UInputDate v-model="state.dateRange" range />
</UFormField>Programmatic validation
<script setup lang="ts">
const form = useTemplateRef('form')
async function validateAndSubmit() {
const result = await form.value?.validate()
if (result) {
// valid — submit
}
}
async function validateEmail() {
await form.value?.validate({ name: 'email' })
}
function setServerError() {
form.value?.setErrors([
{ name: 'email', message: 'Email already taken' }
])
}
function resetErrors() {
form.value?.clearErrors()
}
</script>
<template>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<!-- fields -->
</UForm>
</template>Form in a modal
Use #footer="{ close }" scoped slot for cancel/submit actions. Wrap the modal body in UForm with a type="submit" button in the footer so validation runs on submit.
<UModal v-model:open="isOpen" title="Edit profile" description="Update your information." :ui="{ footer: 'justify-end' }">
<template #body>
<UForm id="profile-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton type="submit" form="profile-form" label="Save" />
</template>
</UModal>Chat Layout
Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration.
When to use
- AI chatbot interfaces
- Customer support chat
- Any conversational UI with streaming responses
Setup
Install dependencies
Nuxt:
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxtVue (Vite):
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vueRegister Comark module
Nuxt:
```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: [ '@nuxt/ui', '@comark/nuxt' ] })
**Vue (Vite):** No module registration needed, import directly from `@comark/vue`.
> `@comark/nuxt` (or `@comark/vue` for Vue projects) provides the `Comark` component used to render AI responses as streaming Markdown, it incrementally renders tokens as they arrive and automatically enables Nuxt UI's prose components.
### Dark mode for syntax highlighting
When using the `highlight` plugin, add the following CSS to your stylesheet:
html.dark .shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; font-style: var(--shiki-dark-font-style) !important; font-weight: var(--shiki-dark-font-weight) !important; text-decoration: var(--shiki-dark-text-decoration) !important; }
### Server endpoint
Using [Vercel AI Gateway](https://vercel.com/ai-gateway) (recommended):
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'), system: 'You are a helpful assistant.', messages: await convertToModelMessages(messages) }).toUIMessageStreamResponse() })
Or with a direct provider (e.g., `pnpm add @ai-sdk/openai`):
import { streamText, convertToModelMessages } from 'ai' import { openai } from '@ai-sdk/openai'
export default defineEventHandler(async (event) => { const { messages } = await readBody(event)
return streamText({ model: openai('gpt-5-nano'), system: 'You are a helpful assistant.', messages: await convertToModelMessages(messages) }).toUIMessageStreamResponse() })
## Component tree
UDashboardPanel ├── #header → UDashboardNavbar ├── #body → UContainer → UChatMessages │ ├── #content → UChatReasoning, UChatTool, Comark │ └── #indicator (loading) └── #footer → UContainer → UChatPrompt └── UChatPromptSubmit
## Full page chat
<script setup lang="ts"> import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai' import { Chat } from '@ai-sdk/vue' import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai' import highlight from '@comark/nuxt/plugins/highlight'
definePageMeta({ layout: 'dashboard' })
const input = ref('')
const chat = new Chat({ onError(error) { console.error(error) } })
function onSubmit() { if (!input.value.trim()) return chat.sendMessage({ text: input.value }) input.value = '' } </script>
<template> <UDashboardPanel> <template #header> <UDashboardNavbar title="Chat" /> </template>
<template #body> <UContainer> <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="isPartStreaming(part)" > <Comark :markdown="part.text" :streaming="isPartStreaming(part)" :plugins="[highlight()]" 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)"> <Comark v-if="message.role === 'assistant'" :markdown="part.text" :streaming="isPartStreaming(part)" :plugins="[highlight()]" 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> </UContainer> </template>
<template #footer> <UContainer class="pb-4 sm:pb-6"> <UChatPrompt v-model="input" :error="chat.error" @submit="onSubmit"> <UChatPromptSubmit :status="chat.status" @stop="chat.stop()" @reload="chat.regenerate()" /> </UChatPrompt> </UContainer> </template> </UDashboardPanel> </template>
## Key components
- `UChatMessages` — scrollable message list with auto-scroll. Props: `messages`, `status`. Slots: `#content` (per message), `#actions`, `#indicator`.
- `UChatMessage` — individual bubble. Props: `message`, `side` (`'left'`/`'right'`).
- `UChatReasoning` — collapsible reasoning block. Auto-opens during streaming, auto-closes when done. Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai`.
- `UChatTool` — tool invocation status. Use `isToolStreaming(part)`. Variants: `'inline'` (default), `'card'`.
- `UChatPrompt` — enhanced textarea. Accepts all Textarea props + `error` prop.
- `UChatPromptSubmit` — submit button with automatic status handling (send/stop/reload).
- `UChatPalette` — layout wrapper for chat inside overlays.
## Chat in a modal
<UModal v-model:open="isOpen"> <template #content> <UChatPalette> <UChatMessages :messages="chat.messages" :status="chat.status" />
<template #prompt> <UChatPrompt v-model="input" @submit="onSubmit"> <UChatPromptSubmit :status="chat.status" /> </UChatPrompt> </template> </UChatPalette> </template> </UModal>
## With model selector
<UChatPrompt v-model="input" @submit="onSubmit"> <UChatPromptSubmit :status="chat.status" />
<template #footer> <USelect v-model="model" :icon="models.find(m => m.value === model)?.icon" placeholder="Select a model" variant="ghost" :items="models" /> </template> </UChatPrompt>
## Conversation sidebar
Combine with dashboard layout for a ChatGPT-like interface:
<template> <UDashboardGroup> <UDashboardSidebar collapsible resizable> <template #header> <UButton icon="i-lucide-plus" label="New chat" block /> </template>
<template #default> <UNavigationMenu :items="conversations" orientation="vertical" /> </template> </UDashboardSidebar>
<slot /> </UDashboardGroup> </template>
Dashboard Layout
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
When to use
- Admin panels, back-office UIs
- Email clients, project management tools
- Any app with a persistent sidebar and content panels
- Combine with chat or editor layouts for specialized dashboards
Component tree
UApp
└── NuxtLayout (dashboard)
└── UDashboardGroup
├── UDashboardSidebar
│ ├── #header (logo, search button)
│ ├── #default (navigation) — receives { collapsed } slot prop
│ └── #footer (user menu)
└── NuxtPage
└── UDashboardPanel
├── #header → UDashboardNavbar + UDashboardToolbar
├── #body (scrollable content)
└── #footer (optional)Layout
```vue [layouts/dashboard.vue] <script setup lang="ts"> import type { NavigationMenuItem } from '@nuxt/ui'
const items = computed<NavigationMenuItem[]>(() => [{ label: 'Home', icon: 'i-lucide-house', to: '/dashboard' }, { label: 'Inbox', icon: 'i-lucide-inbox', to: '/dashboard/inbox' }, { label: 'Users', icon: 'i-lucide-users', to: '/dashboard/users' }, { label: 'Settings', icon: 'i-lucide-settings', to: '/dashboard/settings' }]) </script>
<template> <UDashboardGroup> <UDashboardSidebar collapsible resizable> <template #header="{ collapsed }"> <UDashboardSearchButton :collapsed="collapsed" /> </template>
<template #default="{ collapsed }"> <UNavigationMenu :collapsed="collapsed" :items="items" orientation="vertical" /> </template>
<template #footer="{ collapsed }"> <UButton :icon="collapsed ? 'i-lucide-log-out' : undefined" :label="collapsed ? undefined : 'Sign out'" color="neutral" variant="ghost" block /> </template> </UDashboardSidebar>
<slot /> </UDashboardGroup> </template>
## Page
<script setup lang="ts"> definePageMeta({ layout: 'dashboard' }) </script>
<template> <UDashboardPanel> <template #header> <UDashboardNavbar title="Home"> <template #leading> <UDashboardSidebarCollapse /> </template> <template #right> <UButton icon="i-lucide-plus" label="New" /> </template> </UDashboardNavbar> </template>
<template #body> <!-- Page content --> </template> </UDashboardPanel> </template>
### Common mistakes
- Forgetting `definePageMeta({ layout: 'dashboard' })` — the page won't use the dashboard layout without it.
- Putting content directly in `UDashboardPanel` without using `#body` slot — content won't scroll properly.
- Not handling the `collapsed` slot prop — sidebar content should adapt when collapsed (hide labels, center icons).
## Key components
### DashboardGroup
Root wrapper. Manages sidebar state and persistence.
| Prop | Default | Purpose |
|---|---|---|
| `storage` | `'cookie'` | `'cookie'`, `'localStorage'`, `false` |
| `storage-key` | `'dashboard'` | Storage key name |
### DashboardSidebar
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
| Prop | Default | Purpose |
|---|---|---|
| `resizable` | `false` | Drag to resize |
| `collapsible` | `false` | Collapse when dragged to edge |
| `side` | `'left'` | `'left'` or `'right'` |
| `mode` | `'slideover'` | Mobile: `'modal'`, `'slideover'`, `'drawer'` |
All slots receive `{ collapsed, collapse }` — `collapsed` is the boolean state, `collapse(value)` toggles it programmatically. Use `v-model:collapsed` and `v-model:open` (mobile) for state control.
### DashboardPanel
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw, no scroll) slots.
### DashboardNavbar / DashboardToolbar
Navbar: `#leading`, `#left`, `#default`, `#right` slots + `title` prop. Use `UDashboardSidebarCollapse` in `#leading` to toggle sidebar on mobile.
Toolbar: same slots, sits below navbar for filters/actions.
### UNavigationMenu in sidebar
Always pass `:collapsed="collapsed"` to `UNavigationMenu` inside a collapsible sidebar — it auto-hides labels and centers icons. Use `NavigationMenuItem[][]` (array of arrays) for separate groups (main nav + footer links).
## Multi-panel (list-detail)
<script setup lang="ts"> definePageMeta({ layout: 'dashboard' }) </script>
<template> <UDashboardPanel id="inbox-list" resizable> <template #header> <UDashboardNavbar title="Inbox" /> </template> <template #body> <!-- Email list --> </template> </UDashboardPanel>
<UDashboardPanel id="inbox-detail" class="hidden lg:flex"> <template #header> <UDashboardNavbar title="Message" /> </template> <template #body> <!-- Email content --> </template> </UDashboardPanel> </template>
## With toolbar
<UDashboardPanel> <template #header> <UDashboardNavbar title="Users" /> <UDashboardToolbar> <template #left> <UInput icon="i-lucide-search" placeholder="Search..." /> </template> <template #right> <USelect :items="['All', 'Active', 'Inactive']" /> </template> </UDashboardToolbar> </template> </UDashboardPanel>
## With search
<template> <UDashboardGroup> <UDashboardSidebar> <template #header> <UDashboardSearchButton /> </template> </UDashboardSidebar>
<slot />
<UDashboardSearch :groups="searchGroups" /> </UDashboardGroup> </template>
## Right sidebar
<UDashboardGroup> <UDashboardSidebar collapsible resizable> <!-- Left sidebar --> </UDashboardSidebar>
<slot />
<UDashboardSidebar side="right" resizable> <!-- Right sidebar --> </UDashboardSidebar> </UDashboardGroup>
Docs Layout
Build documentation sites with sidebar navigation, table of contents, and surround links.
When to use
- Technical documentation sites
- Knowledge bases, help centers
- Any content-heavy site with hierarchical navigation
Requires@nuxt/content— see conventions for setup (module order +@source).
Component tree
UApp
├── UHeader
├── UMain
│ └── NuxtLayout (docs)
│ └── UPage
│ ├── #left → UPageAside → UContentNavigation
│ └── NuxtPage
│ ├── UPageHeader
│ ├── UPageBody → ContentRenderer + UContentSurround
│ └── #right → UContentToc
└── UFooterApp shell
```vue [app.vue] <script setup lang="ts"> import type { NavigationMenuItem } from '@nuxt/ui'
const route = useRoute()
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
provide('navigation', navigation)
const items = computed<NavigationMenuItem[]>(() => [{ label: 'Docs', to: '/docs/getting-started', active: route.path.startsWith('/docs') }]) </script>
<template> <UApp> <UHeader> <template #title> <Logo class="h-6 w-auto" /> </template>
<UNavigationMenu :items="items" />
<template #right> <UContentSearchButton /> <UColorModeButton /> </template> </UHeader>
<UMain> <NuxtLayout> <NuxtPage /> </NuxtLayout> </UMain>
<UFooter />
<UContentSearch :navigation="navigation" /> </UApp> </template>
## Layout
<script setup lang="ts"> import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation') </script>
<template> <UPage> <template #left> <UPageAside> <UContentNavigation :navigation="navigation" /> </UPageAside> </template>
<slot /> </UPage> </template>
## Page
<script setup lang="ts"> const route = useRoute()
definePageMeta({ layout: 'docs' })
const { data: page } = await useAsyncData(route.path, () => { return queryCollection('docs').path(route.path).first() })
const { data: surround } = await useAsyncData(${route.path}-surround, () => { return queryCollectionItemSurroundings('docs', route.path) }) </script>
<template> <UPage> <UPageHeader :title="page.title" :description="page.description" />
<UPageBody> <ContentRenderer :value="page" />
<USeparator />
<UContentSurround :surround="surround" /> </UPageBody>
<template #right> <UContentToc :links="page.body.toc.links" /> </template> </UPage> </template>
### How nesting works
The outer `UPage` in the layout handles the **left sidebar**. The inner `UPage` in the page handles the **right sidebar**. They nest correctly — this is intentional.
### Common mistakes
- Not providing navigation via `provide`/`inject` — the layout needs it from the app shell.
- Forgetting `UContentSearch` in app.vue — search won't work without it.
- Using `UContentSearchButton` without `UContentSearch` — the button opens search, but the search component must exist.
## Key components
- `UPage` — multi-column grid with `#left`, `#default`, `#right` slots
- `UPageAside` — sticky sidebar wrapper (visible from `lg` breakpoint)
- `UContentNavigation` — sidebar navigation tree from Nuxt Content
- `UContentToc` — table of contents from page headings
- `UContentSurround` — prev/next links
- `UContentSearch` / `UContentSearchButton` — search command palette
- `UPageAnchors` — simpler alternative to full TOC
Editor Layout
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
When to use
- Note-taking apps, CMS editors
- Collaborative editing interfaces
- Any rich text editing need (supports JSON, HTML, and Markdown)
Component tree
UEditor
├── UEditorToolbar (fixed / bubble / floating)
├── UEditorDragHandle
├── UEditorSuggestionMenu
├── UEditorMentionMenu
└── UEditorEmojiMenuBasic editor
<script setup lang="ts">
const content = ref({
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Hello World' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start writing...' }]
}
]
})
</script>
<template>
<UEditor v-slot="{ editor }" v-model="content">
<UEditorToolbar :editor="editor" />
<UEditorSuggestionMenu :editor="editor" />
<UEditorMentionMenu
:editor="editor"
:items="[
{ label: 'Benjamin', avatar: { src: 'https://github.com/benjamincanac.png' } },
{ label: 'Sébastien', avatar: { src: 'https://github.com/atinux.png' } }
]"
/>
<UEditorEmojiMenu :editor="editor" />
<UEditorDragHandle :editor="editor" />
</UEditor>
</template>If you encounter prosemirror-related errors, add prosemirror packages tovite.optimizeDeps.includeinnuxt.config.ts.
Key components
UEditor— rich text editor.v-modelaccepts JSON (default), HTML, or Markdown viacontent-typeprop. Default slot provides{ editor, handlers }—editoris the Tiptap instance,handlerscontains action functions for toolbar/menus.UEditorToolbar— toolbar withlayout:'fixed'(default),'bubble'(on selection),'floating'(on empty lines).UEditorDragHandle— block drag-and-drop handle.UEditorSuggestionMenu— slash command menu (type/to open).UEditorMentionMenu—@mention menu.UEditorEmojiMenu— emoji picker (type:to open).
Toolbar modes
<!-- Fixed (default) — always visible at top -->
<UEditorToolbar :editor="editor" />
<!-- Bubble — appears on text selection -->
<UEditorToolbar :editor="editor" layout="bubble" />
<!-- Floating — appears on empty lines -->
<UEditorToolbar :editor="editor" layout="floating" />Content types
<!-- JSON (default) -->
<UEditor v-model="jsonContent" />
<!-- HTML -->
<UEditor v-model="htmlContent" content-type="html" />
<!-- Markdown -->
<UEditor v-model="markdownContent" content-type="markdown" />With document sidebar
Combine with Dashboard layout for a multi-document editor:
```vue [layouts/editor.vue] <template> <UDashboardGroup> <UDashboardSidebar collapsible resizable> <template #header> <UButton icon="i-lucide-plus" label="New document" block /> </template>
<template #default="{ collapsed }"> <UNavigationMenu :collapsed="collapsed" :items="documents.map(doc => ({ label: doc.title, to: /editor/${doc.id}, icon: 'i-lucide-file-text' }))" orientation="vertical" /> </template> </UDashboardSidebar>
<slot /> </UDashboardGroup> </template>
<script setup lang="ts"> definePageMeta({ layout: 'editor' })
const content = ref({ type: 'doc', content: [] }) </script>
<template> <UDashboardPanel> <template #header> <UDashboardNavbar title="Editor"> <template #right> <UButton label="Save" icon="i-lucide-save" /> </template> </UDashboardNavbar> </template>
<UContainer class="py-8"> <UEditor v-slot="{ editor }" v-model="content"> <UEditorToolbar :editor="editor" /> <UEditorSuggestionMenu :editor="editor" /> <UEditorEmojiMenu :editor="editor" /> <UEditorDragHandle :editor="editor" /> </UEditor> </UContainer> </UDashboardPanel> </template>
Landing Page Layout
Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components.
When to use
- Marketing sites, product pages, company sites
- Blog and content pages
- Pricing, changelog, portfolio pages
- Any public-facing page that isn't a dashboard or documentation
App shell
```vue [app.vue] <script setup lang="ts"> import type { NavigationMenuItem } from '@nuxt/ui'
const items = computed<NavigationMenuItem[]>(() => [{ label: 'Features', to: '#features' }, { label: 'Pricing', to: '/pricing' }, { label: 'Blog', to: '/blog' }]) </script>
<template> <UApp> <UHeader> <template #title> <Logo class="h-6 w-auto" /> </template>
<UNavigationMenu :items="items" />
<template #right> <UColorModeButton /> <UButton label="Sign in" color="neutral" variant="ghost" /> <UButton label="Get started" /> </template>
<template #body> <UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" /> </template> </UHeader>
<UMain> <NuxtPage /> </UMain>
<UFooter> <template #left> <p class="text-muted text-sm">Copyright © {{ new Date().getFullYear() }}</p> </template> <template #right> <UButton icon="i-simple-icons-github" color="neutral" variant="ghost" to="https://github.com" target="_blank" /> </template> </UFooter> </UApp> </template>
### Common mistakes
- Forgetting the `#body` slot on `UHeader` — this is the mobile menu content. Without it, mobile users have no navigation.
- Using `variant="solid"` for both header and hero buttons — the header button should be lower weight than the hero CTA.
## Landing page
<template> <UPageHero title="Build faster with Nuxt UI" description="A comprehensive Vue UI component library." :links="[ { label: 'Get started', to: '/docs', icon: 'i-lucide-square-play' }, { label: 'Learn more', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' } ]" orientation="horizontal" > <img src="/hero-image.png" alt="App screenshot" class="rounded-lg shadow-2xl ring ring-default" /> </UPageHero>
<UPageSection id="features" headline="Features" title="Everything you need" description="A comprehensive suite of components and utilities." :features="[ { title: 'Accessible', description: 'Built on Reka UI with full ARIA support.', icon: 'i-lucide-accessibility' }, { title: 'Customizable', description: 'Tailwind Variants theming with full control.', icon: 'i-lucide-palette' }, { title: 'Responsive', description: 'Mobile-first components.', icon: 'i-lucide-monitor-smartphone' } ]" />
<UPageCTA title="Trusted by thousands of developers" description="Join the community and start building today." :links="[ { label: 'Get started', color: 'neutral' }, { label: 'Star on GitHub', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' } ]" />
<UPageSection id="pricing" headline="Pricing" title="Simple, transparent pricing"> <UPricingPlans :plans="[ { title: 'Free', price: '$0', description: 'For personal projects', features: ['10 components', 'Community support'] }, { title: 'Pro', price: '$99', description: 'For teams', features: ['All components', 'Priority support'], highlight: true }, { title: 'Enterprise', price: 'Custom', description: 'For large teams', features: ['Custom components', 'Dedicated support'] } ]" /> </UPageSection> </template>
## Key components
- `UPageHero` — hero with title, description, links, and optional media. Use `orientation="horizontal"` for side-by-side layout.
- `UPageSection` — content section with headline, title, description, and `features` grid. Use `id` for anchor links.
- `UPageCTA` — call to action block.
- `UPageGrid` / `UPageCard` — card grid for features, testimonials, etc.
- `UPageFeature` — individual feature item.
- `UPageLogos` — logo wall for social proof.
- `UPricingPlans` / `UPricingTable` — pricing cards and comparison tables.
- `UFooterColumns` — multi-column footer with link groups (used inside `UFooter`).
## Variations
### Alternating feature sections
<UPageSection title="Feature A" orientation="horizontal"> <img src="/feature-a.png" /> </UPageSection>
<UPageSection title="Feature B" orientation="horizontal" reverse> <img src="/feature-b.png" /> </UPageSection>
### Blog listing
<script setup lang="ts"> const { data: posts } = await useAsyncData('posts', () => queryCollection('posts').all()) </script>
<template> <UPage> <UPageHero title="Blog" description="The latest news and updates." /> <UPageBody> <UContainer> <UBlogPosts> <UBlogPost v-for="post in posts" :key="post.path" v-bind="post" :to="post.path" /> </UBlogPosts> </UContainer> </UPageBody> </UPage> </template>
### Changelog
<script setup lang="ts"> const { data: versions } = await useAsyncData('versions', () => queryCollection('changelog').all()) </script>
<template> <UPage> <UPageHero title="Changelog" /> <UPageBody> <UContainer> <UChangelogVersions> <UChangelogVersion v-for="version in versions" :key="version.path" v-bind="version" /> </UChangelogVersions> </UContainer> </UPageBody> </UPage> </template>
Auth Forms
UAuthForm (recommended)
UAuthForm provides a complete auth form with fields, providers, validation, and submit — no manual UForm + UFormField wiring needed. Wrap it in UPageCard for a polished look.
```vue [pages/login.vue] <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: () => navigateTo('/auth/google', { external: true }) }, { label: 'GitHub', icon: 'i-simple-icons-github', onClick: () => navigateTo('/auth/github', { external: true }) }]
const schema = z.object({ email: z.email('Invalid email'), password: z.string('Password is required').min(8, 'Must be at least 8 characters') })
type Schema = z.output<typeof schema>
function onSubmit(payload: FormSubmitEvent<Schema>) { // payload.data contains validated fields } </script>
<template> <div class="flex min-h-dvh items-center justify-center"> <UPageCard class="w-full max-w-md"> <UAuthForm :schema="schema" :fields="fields" :providers="providers" title="Welcome back!" description="Sign in to your account." icon="i-lucide-lock" @submit="onSubmit" > <template #password-hint> <ULink to="/forgot-password" class="text-primary font-medium">Forgot password?</ULink> </template> <template #footer> Don't have an account? <ULink to="/signup" class="text-primary font-medium">Sign up</ULink>. </template> </UAuthForm> </UPageCard> </div> </template>
### UAuthForm key props
| Prop | Purpose |
|---|---|
| `title`, `description`, `icon` | Header content |
| `fields` | `AuthFormField[]` — each has `name`, `type`, `label`, `placeholder`, `required` |
| `providers` | `ButtonProps[]` — social login buttons shown above/below the form |
| `schema` | Zod/Valibot schema for validation |
| `submit` | Customize submit button: `{ label: 'Sign in', block: true }` |
| `separator` | Text between providers and fields (default: `'or'`) |
### UAuthForm key slots
| Slot | Purpose |
|---|---|
| `#description` | Override description (e.g., add sign-up link) |
| `#password-hint` | "Forgot password?" link on password field |
| `#validation` | Custom error display (e.g., `UAlert`) |
| `#footer` | Terms of service, sign-up link |
| `#<field>-field` | Override a specific field's rendering |
## Custom auth layout
For layouts where `UAuthForm` is too opinionated, use `UCard` + `UForm` + `UFormField` directly.
<script setup lang="ts"> import * as z from 'zod' import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({ email: z.email('Invalid email'), password: z.string().min(1, 'Password is required') })
type Schema = z.output<typeof schema> const state = reactive<Partial<Schema>>({ email: '', password: '' })
async function onSubmit(event: FormSubmitEvent<Schema>) { // event.data contains validated fields } </script>
<template> <div class="flex min-h-dvh items-center justify-center"> <UCard class="w-full max-w-sm"> <template #header> <div class="text-center"> <h1 class="text-xl font-semibold text-default">Welcome back</h1> <p class="mt-1 text-sm text-muted">Sign in to your account</p> </div> </template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit"> <UFormField name="email" label="Email"> <UInput v-model="state.email" type="email" placeholder="you@example.com" /> </UFormField>
<UFormField name="password" label="Password"> <template #hint> <NuxtLink to="/forgot-password" class="text-sm text-primary">Forgot password?</NuxtLink> </template> <UInput v-model="state.password" type="password" /> </UFormField>
<UButton type="submit" label="Sign in" block /> </UForm>
<template #footer> <p class="text-center text-sm text-muted"> Don't have an account? <NuxtLink to="/signup" class="text-primary font-medium">Sign up</NuxtLink> </p> </template> </UCard> </div> </template>
## Tips
- Prefer `UAuthForm` with `UPageCard` for standard auth pages — handles layout, providers, validation, and submit
- Use `import * as z from 'zod'` and `z.email()` (Zod 4 syntax)
- Type the submit handler: `function onSubmit(event: FormSubmitEvent<Schema>)` — access validated data via `event.data`
- Center auth forms with `flex min-h-dvh items-center justify-center`
- Place "Forgot password?" link as `#password-hint` slot on `UAuthForm`, or `#hint` slot on `UFormField`
- Social login buttons: use `providers` prop on `UAuthForm`, or add manually with `<USeparator label="or" />`
Data Tables
Complete patterns for displaying and managing tabular data.
Basic table
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
const data = ref([
{ name: 'Alice', email: 'alice@example.com', role: 'Admin' },
{ name: 'Bob', email: 'bob@example.com', role: 'Editor' }
])
const columns: TableColumn<typeof data.value[number]>[] = [{
accessorKey: 'name',
header: 'Name'
}, {
accessorKey: 'email',
header: 'Email'
}, {
accessorKey: 'role',
header: 'Role'
}]
</script>
<template>
<UTable :data="data" :columns="columns" />
</template>With search and filters (dashboard)
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
const search = ref('')
const roleFilter = ref('All')
const rows = ref([
{ name: 'Alice', email: 'alice@example.com', role: 'Admin', status: 'Active' },
{ name: 'Bob', email: 'bob@example.com', role: 'Editor', status: 'Inactive' }
])
const columns: TableColumn[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'role', header: 'Role' },
{ accessorKey: 'status', header: 'Status' },
{ id: 'actions' }
]
const filteredRows = computed(() => {
return rows.value.filter(row => {
const matchesSearch = !search.value || row.name.toLowerCase().includes(search.value.toLowerCase())
const matchesRole = roleFilter.value === 'All' || row.role === roleFilter.value
return matchesSearch && matchesRole
})
})
</script>
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar title="Users" />
<UDashboardToolbar>
<template #left>
<UInput v-model="search" icon="i-lucide-search" placeholder="Search users..." />
</template>
<template #right>
<USelect v-model="roleFilter" :items="['All', 'Admin', 'Editor', 'Viewer']" />
</template>
</UDashboardToolbar>
</template>
<template #body>
<UTable :data="filteredRows" :columns="columns">
<template #status-cell="{ row }">
<UBadge :color="row.original.status === 'Active' ? 'success' : 'neutral'" :label="row.original.status" variant="subtle" />
</template>
<template #actions-cell="{ row }">
<UDropdownMenu
:items="[
[{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit(row.original) }],
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: () => remove(row.original) }]
]"
>
<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" />
</UDropdownMenu>
</template>
</UTable>
</template>
</UDashboardPanel>
</template>With row selection
Row selection uses TanStack Table's rowSelection state — a Record<string, boolean> keyed by row index.
<script setup lang="ts">
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({})
</script>
<template>
<UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />
<div class="px-4 py-3.5 text-sm text-muted">
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
</div>
</template>Add a checkbox column using the h function. Use tri-state modelValue (true, false, or 'indeterminate') for the "select all" header:
import { h } from 'vue'
const UCheckbox = resolveComponent('UCheckbox')
const columns: TableColumn[] = [{
id: 'select',
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
},
// ... other columns
]With pagination
Use v-model:pagination on UTable with TanStack's getPaginationRowModel, then wire UPagination to the table API. UPagination's total is total items (not pages) — it calculates page count from total / items-per-page.
<script setup lang="ts">
import { getPaginationRowModel } from '@tanstack/vue-table'
const table = useTemplateRef('table')
const pagination = ref({
pageIndex: 0,
pageSize: 5
})
</script>
<template>
<UTable
ref="table"
v-model:pagination="pagination"
:data="data"
:columns="columns"
:pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
/>
<div class="flex justify-end p-4">
<UPagination
:page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
:total="table?.tableApi?.getFilteredRowModel().rows.length"
@update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
/>
</div>
</template>With async data (Nuxt)
Use status === 'pending' || status === 'idle' for loading state — idle covers the initial render before useLazyFetch starts.
<script setup lang="ts">
const { data, status } = useLazyFetch('/api/users', { server: false })
</script>
<template>
<UTable :data="data" :columns="columns" :loading="status === 'pending' || status === 'idle'" />
</template>For server-side pagination:
<script setup lang="ts">
const page = ref(1)
const { data, status } = await useAsyncData(
'users',
() => $fetch('/api/users', { query: { page: page.value } }),
{ watch: [page] }
)
</script>
<template>
<UTable :data="data?.items" :columns="columns" :loading="status === 'pending'" />
<div class="flex justify-end p-4">
<UPagination v-model="page" :total="data?.total" :items-per-page="data?.pageSize" />
</div>
</template>Tips
- Table is built on TanStack Table — columns use
ColumnDefformat withaccessorKey,header,cell - Use
#<column>-celland#<column>-headertemplate slots to customize rendering with Vue templates - Alternatively, use the
hfunction insideheaderandcellcolumn properties for inline rendering - Row data in slots is accessed via
row.original(notrowdirectly) - Use
v-model:row-selectionfor selection,v-model:sortingfor sort state - Wrap tables in
UDashboardPanelwith#headertoolbar for the dashboard pattern - For empty states, use the
#emptyslot
Navigation
Patterns for headers, sidebars, breadcrumbs, and tab navigation.
Header with mobile menu
UHeader default slot is desktop nav, #body is the mobile menu. Without #body, mobile users have no navigation.
<UHeader>
<template #title>
<Logo class="h-6 w-auto" />
</template>
<UNavigationMenu :items="items" />
<template #right>
<UColorModeButton />
<UButton label="Sign in" color="neutral" variant="ghost" />
</template>
<template #body>
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
</template>
</UHeader>Full app shell example in landing layout.
Sidebar navigation (dashboard)
See dashboard layout for the full sidebar pattern with UDashboardSidebar + UNavigationMenu. Key points:
- Pass
:collapsed="collapsed"toUNavigationMenuinside collapsible sidebars - Use
NavigationMenuItem[][](nested arrays) for separate nav groups - Use
#footerslot for user menu withUDropdownMenu
Breadcrumbs
<script setup lang="ts">
const route = useRoute()
const breadcrumbs = computed(() => {
const segments = route.path.split('/').filter(Boolean)
return segments.map((segment, index) => ({
label: segment.charAt(0).toUpperCase() + segment.slice(1),
to: '/' + segments.slice(0, index + 1).join('/')
}))
})
</script>
<template>
<UBreadcrumb :items="breadcrumbs" />
</template>Tab navigation (within a page)
<script setup lang="ts">
const items = [{
label: 'Overview',
icon: 'i-lucide-layout-dashboard',
slot: 'overview' as const
}, {
label: 'Activity',
icon: 'i-lucide-activity',
slot: 'activity' as const
}, {
label: 'Members',
icon: 'i-lucide-users',
slot: 'members' as const
}]
</script>
<template>
<UTabs :items="items">
<template #overview>
<!-- Overview content -->
</template>
<template #activity>
<!-- Activity feed -->
</template>
<template #members>
<!-- Members list -->
</template>
</UTabs>
</template>Overlays
Patterns for modals, slideovers, drawers, and command palettes.
Confirmation dialog
<script setup lang="ts">
const isOpen = ref(false)
function confirmDelete() {
// perform delete
isOpen.value = false
}
</script>
<template>
<UButton label="Delete" color="error" variant="soft" @click="isOpen = true" />
<UModal v-model:open="isOpen" title="Delete item" description="This action cannot be undone. Are you sure?" :ui="{ footer: 'justify-end' }">
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton label="Delete" color="error" @click="confirmDelete" />
</template>
</UModal>
</template>Programmatic confirmation (useOverlay)
Reusable pattern — no template state needed at the call site.
```vue [components/ConfirmModal.vue] <script setup lang="ts"> defineProps<{ title: string description?: string }>()
const emit = defineEmits<{ close: [confirmed: boolean] }>() </script>
<template> <UModal :close="{ onClick: () => emit('close', false) }" :title="title" :description="description"> <template #footer> <UButton label="Cancel" color="neutral" variant="outline" @click="emit('close', false)" /> <UButton label="Confirm" color="error" @click="emit('close', true)" /> </template> </UModal> </template>
// Usage anywhere const overlay = useOverlay() const confirm = overlay.create(ConfirmModal)
async function deleteItem(item) { const instance = confirm.open({ title: 'Delete item', description: Are you sure you want to delete "${item.name}"? })
if (await instance.result) { // user confirmed } }
## Form in a slideover
<script setup lang="ts"> import * as z from 'zod'
const isOpen = ref(false)
const schema = z.object({ name: z.string().min(1), email: z.email() })
type Schema = z.output<typeof schema> const state = reactive<Partial<Schema>>({})
function onSave() { // save user isOpen.value = false } </script>
<template> <UButton label="Add user" @click="isOpen = true" />
<USlideover v-model:open="isOpen" title="Add user" description="Fill in the details below."> <template #body> <UForm id="user-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave"> <UFormField name="name" label="Name"> <UInput v-model="state.name" /> </UFormField> <UFormField name="email" label="Email"> <UInput v-model="state.email" type="email" /> </UFormField> </UForm> </template>
<template #footer="{ close }"> <UButton label="Cancel" color="neutral" variant="outline" @click="close" /> <UButton type="submit" form="user-form" label="Save" /> </template> </USlideover> </template>
## Command palette
<script setup lang="ts"> const isOpen = ref(false)
defineShortcuts({ meta_k: () => { isOpen.value = true } })
const groups = [{ id: 'actions', label: 'Actions', items: [ { label: 'New file', icon: 'i-lucide-file-plus', kbds: ['meta', 'n'], onSelect: () => newFile() }, { label: 'New folder', icon: 'i-lucide-folder-plus', onSelect: () => newFolder() } ] }, { id: 'navigation', label: 'Navigation', items: [ { label: 'Dashboard', icon: 'i-lucide-house', to: '/dashboard' }, { label: 'Settings', icon: 'i-lucide-settings', to: '/settings' } ] }] </script>
<template> <UButton label="Search..." icon="i-lucide-search" color="neutral" variant="outline" @click="isOpen = true" />
<UCommandPalette v-model:open="isOpen" :groups="groups" placeholder="Type a command or search..." /> </template>
## Drawer (bottom sheet)
<script setup lang="ts"> const isOpen = ref(false) </script>
<template> <UButton label="Options" @click="isOpen = true" />
<UDrawer v-model:open="isOpen" title="Options"> <template #body> <div class="space-y-2 p-4"> <UButton label="Share" icon="i-lucide-share" block variant="ghost" /> <UButton label="Export" icon="i-lucide-download" block variant="ghost" /> <USeparator /> <UButton label="Delete" icon="i-lucide-trash" block variant="ghost" color="error" /> </div> </template> </UDrawer> </template>
Related skills
How it compares
Use nuxt-ui when you want an opinionated Vue plus Tailwind component system with MCP-backed API lookup instead of assembling headless primitives manually.
FAQ
What does nuxt-ui do?
Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites,
When should I use nuxt-ui?
Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites,
Is nuxt-ui safe to install?
Review the Security Audits panel on this page before installing in production.