references/components.md
# 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 |
| `USplitter` | Resizable panels separated by draggable handles |
| `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 |
| `UProgressGroup` | Segmented progress bar with a list of items |
| `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 |
references/guidelines/component-selection.md
# 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 `UModal` for destructive confirmations ("Are you sure you want to delete?")
- Use `USlideover` for detail views in dashboards (email preview, user profile)
- Use `UDrawer` for mobile navigation or action sheets
- Modal and Slideover support `mode="drawer"` for automatic mobile drawer behavior
- For programmatic overlays, use `useOverlay()` instead of `v-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 `UNavigationMenu` with `orientation="vertical"` in sidebars, default horizontal in headers
- Use `UTabs` when switching views that don't need their own URL
- Use route-based navigation (`to` prop) when views should have shareable URLs
- `UCommandPalette` is typically opened via `Cmd+K` shortcut — use `defineShortcuts` to 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 `UAuthForm` for login/signup pages — handles fields, social providers, validation, and layout out of the box
- Use `USelect` for short, known lists (country, status, role)
- Use `USelectMenu` when the list is long or needs search
- Use `UInputMenu` when the user might want to type a value that's not in the list
- Wrap all form inputs in `UFormField` for 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 |
| Breakdown of a total | `UProgressGroup` | One bar split into colored segments that add up to `max` |
### Rules
- Use `useToast()` for action feedback: "Item saved", "Email sent", "Error occurred"
- Use `UAlert` for contextual warnings in forms or sections
- Use `UBanner` for 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 `#slot` blocks directly to `<slot name="..." />`
- Theme via `appConfig.ui.prose.<name>` using the same override pattern as other Nuxt UI components
- `Callout` colors: `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 |
| Resizable side-by-side panes | `USplitter` | IDE-style layouts, resizable sidebars |
### Rules
- Don't overuse `UCard` — plain content with spacing is often better than wrapping everything in cards
- Use `UPageCard` instead of `UCard` when you need icon, badge, highlight, or links — it's designed for feature grids and landing pages
- Use `UPageSection` for marketing/landing page sections, not for app UI
- Use `UContainer` inside `UDashboardPanel` body for consistent content width
references/guidelines/conventions.md
# 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`:
```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:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxt/content']
})
```
Add `@source` in your CSS so Tailwind generates classes used in markdown/MDC:
```css
/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
@source "../../../content/**/*";
```
Use `mapContentNavigation` to transform content navigation for components like `UBreadcrumb`:
```ts
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:
```json
{
"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
```vue
<UApp :locale="fr">
<NuxtPage /> <!-- or <RouterView /> for Vue -->
</UApp>
```
## Icons
Nuxt UI registers `@nuxt/icon` automatically. Format: `i-{collection}-{name}`. Prefer `lucide` collection.
```vue
<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:
```bash
pnpm i @iconify-json/lucide
pnpm i @iconify-json/simple-icons
```
Custom local collections (Nuxt only):
```ts
// 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:
```ts
// 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:
```ts
const items = [
{ label: 'Edit', icon: 'i-lucide-pencil' },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
]
```
**Nested array** — groups with automatic separators between them:
```ts
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
```ts
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](../recipes/overlays.md) for full patterns.
```ts
const overlay = useOverlay()
const modal = overlay.create(MyComponent)
const instance = modal.open({ title: 'Confirm?' })
if (await instance.result) { /* confirmed */ }
```
### defineShortcuts
```ts
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:
```ts
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
```vue
<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):
```ts
import { en } from '@nuxt/ui/locale'
const locale = extendLocale(en, {
code: 'en-AU',
messages: {
commandPalette: { placeholder: 'Search a component...' }
}
})
```
#### Custom locale from scratch
```ts
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
```ts
// 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: 'العربية' }
]
}
})
```
```vue
<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 switch
- `UColorModeSelect` — dropdown with system/light/dark options
- `UColorModeAvatar` — displays different avatar per mode
- `UColorModeImage` — displays different image per mode
For custom color mode UI, use `useColorMode` with `ClientOnly` to avoid hydration mismatch:
```vue
<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:
```bash
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 # Changelog
npx nuxi@latest init -t ui/calendar # Calendar
```
## Responsive patterns
- Dashboard sidebar hides on mobile, shows a slideover/drawer via `UDashboardSidebar` `mode` prop
- `UHeader` body slot is the mobile menu content (shown when hamburger is tapped)
- Most components handle responsiveness automatically — avoid manual breakpoint classes unless needed
- Use `UPageAside` for sidebars that should hide below `lg` breakpoint
references/guidelines/design-system.md
# 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"` with `variant="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
```ts
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: 'indigo',
secondary: 'violet',
success: 'emerald',
error: 'rose',
neutral: 'zinc'
}
}
})
```
```ts
// 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 `neutral` key — pick one that matches the aesthetic):
- `slate` — cool blue-gray, professional (default)
- `gray` — true neutral, clean
- `zinc` — slightly cool, modern, techy
- `neutral` — perfectly balanced
- `stone` — warm gray, earthy
- `taupe` — warm brown-gray, sophisticated
- `mauve` — purple-tinted gray, elegant
- `mist` — soft blue-gray, airy
- `olive` — green-tinted gray, natural
### Adding custom brand colors
1. Define all 11 shades in CSS:
```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`:
```ts
// 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 text
- `text-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 with `bg-inverted`)
### Backgrounds
- `bg-default` — page background
- `bg-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 borders
- `border-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 necessarily `variant="solid"` — use `variant="soft"` or `"outline"` unless it's the primary action on a confirmation dialog
- **Button groups** should use consistent variants — don't mix `solid` and `outline` siblings
## Customizing components
### `ui` prop
Override theme **slots** on a single instance — wins over global config and variants.
```vue
<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 with `rounded-*` classes in `ui` or `class` when you need a specific radius on a component.
### `class` prop
Override the **root** (or `base`) slot only — simpler than `ui` for single-slot changes.
```vue
<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):
```ts
// 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.
### Replace instead of merge
Classes from the `ui` prop, the `class` prop, and global config are merged onto the component defaults. To replace them instead, set the slot to a function, which receives the default classes as its argument so you can reuse part of them.
In global config it replaces the slot's own classes, so `variants` and `compoundVariants` still apply on top. In the `ui` and `class` props it runs after the variants, so it replaces the resolved classes, variants included.
```vue
<UButton :ui="{ label: () => 'text-base font-bold' }" />
```
```ts
// app.config.ts, applies to every instance
export default defineAppConfig({
ui: {
button: {
slots: {
label: () => 'text-base font-bold'
}
}
}
})
```
### Theme component
Override theme for a section of the component tree without affecting the rest of the app. Renders no DOM element — uses `provide`/`inject`:
```vue
<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:
```ts
// 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.
```ts
// 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:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
prefix: 'tw'
}
}
})
```
```css
/* 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:
```ts
// 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:
```ts
componentDetection: ['Modal', 'Dropdown', 'Popover']
```
## CSS `@theme` customization
Customize Tailwind design tokens in `main.css`:
### Fonts
```css
@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
```css
@theme {
--breakpoint-3xl: 1920px;
}
```
## CSS variables
Nuxt UI exposes CSS variables you can override in `main.css`:
```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:
```css
: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:
```css
:root {
--ui-primary: black;
}
.dark {
--ui-primary: white;
}
```
references/guidelines/forms.md
# Forms
## Basic pattern
Nuxt UI forms use `UForm` + `UFormField` + Standard Schema validation (Zod, Valibot, Yup, or Joi).
```vue
<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 `UFormField` around inputs — it connects validation errors via the `name` prop
- The `name` prop on `UFormField` must match the schema field name exactly
- Use `reactive<Partial<Schema>>({})` for state — `Partial` allows empty initial values
- `@submit` only 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)
```vue
<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
```vue
<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
```vue
<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
```vue
<UFormField name="role" label="Role">
<USelect v-model="state.role" :items="['Admin', 'Editor', 'Viewer']" placeholder="Choose role" />
</UFormField>
```
### Checkbox
```vue
<UFormField name="terms">
<UCheckbox v-model="state.terms" label="I agree to the terms and conditions" />
</UFormField>
```
### Radio group
```vue
<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
```vue
<UFormField name="notifications" label="Email notifications">
<USwitch v-model="state.notifications" />
</UFormField>
```
### Textarea
```vue
<UFormField name="bio" label="Bio" description="Brief description for your profile.">
<UTextarea v-model="state.bio" :rows="3" autoresize :maxrows="6" />
</UFormField>
```
### File upload
```vue
<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
```vue
<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
```vue
<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.
```vue
<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>
```
references/layouts/chat.md
# 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:**
```bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
```
**Vue (Vite):**
```bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
```
### Register 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:
```css [main.css]
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):
```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: gateway('anthropic/claude-sonnet-5'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
```
Or with a direct provider (e.g., `pnpm add @ai-sdk/openai`):
```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { openai } from '@ai-sdk/openai'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: openai('gpt-5-nano'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
```
## Component tree
```
UDashboardPanel
├── #header → UDashboardNavbar
├── #body → UContainer → UChatMessages
│ ├── #content → UChatReasoning, UChatTool, Comark
│ └── #indicator (loading)
└── #footer → UContainer → UChatPrompt
└── UChatPromptSubmit
```
## Full page chat
```vue [pages/chat/[id].vue]
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { useChat } 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 { messages, status, error, sendMessage, stop, regenerate } = useChat({
onError(error) {
console.error(error)
}
})
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
</script>
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar title="Chat" />
</template>
<template #body>
<UContainer>
<UChatMessages :messages="messages" :status="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="error" @submit="onSubmit">
<UChatPromptSubmit :status="status" @stop="stop()" @reload="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
```vue
<UModal v-model:open="isOpen">
<template #content>
<UChatPalette>
<UChatMessages :messages="messages" :status="status" />
<template #prompt>
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="status" />
</UChatPrompt>
</template>
</UChatPalette>
</template>
</UModal>
```
## With model selector
```vue
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="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:
```vue [layouts/dashboard.vue]
<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>
```
references/layouts/dashboard.md
# 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
```vue [pages/dashboard/index.vue]
<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)
```vue [pages/dashboard/inbox.vue]
<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
```vue
<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
```vue [layouts/dashboard.vue]
<template>
<UDashboardGroup>
<UDashboardSidebar>
<template #header>
<UDashboardSearchButton />
</template>
</UDashboardSidebar>
<slot />
<UDashboardSearch :groups="searchGroups" />
</UDashboardGroup>
</template>
```
## Right sidebar
```vue
<UDashboardGroup>
<UDashboardSidebar collapsible resizable>
<!-- Left sidebar -->
</UDashboardSidebar>
<slot />
<UDashboardSidebar side="right" resizable>
<!-- Right sidebar -->
</UDashboardSidebar>
</UDashboardGroup>
```
references/layouts/docs.md
# 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](../guidelines/conventions.md#content-module-integration) for setup (module order + `@source`).
## Component tree
```
UApp
├── UHeader
├── UMain
│ └── NuxtLayout (docs)
│ └── UPage
│ ├── #left → UPageAside → UContentNavigation
│ └── NuxtPage
│ ├── UPageHeader
│ ├── UPageBody → ContentRenderer + UContentSurround
│ └── #right → UContentToc
└── UFooter
```
## App 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
```vue [layouts/docs.vue]
<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
```vue [pages/docs/[...slug].vue]
<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
references/layouts/editor.md
# 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
└── UEditorEmojiMenu
```
## Basic editor
```vue
<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 to `vite.optimizeDeps.include` in `nuxt.config.ts`.
## Key components
- `UEditor` — rich text editor. `v-model` accepts JSON (default), HTML, or Markdown via `content-type` prop. Default slot provides `{ editor, handlers }` — `editor` is the Tiptap instance, `handlers` contains action functions for toolbar/menus.
- `UEditorToolbar` — toolbar with `layout`: `'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
```vue
<!-- 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
```vue
<!-- 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>
```
```vue [pages/editor/[id].vue]
<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>
```
references/layouts/landing.md
# 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
```vue [pages/index.vue]
<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
```vue
<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
```vue [pages/blog/index.vue]
<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
```vue [pages/changelog.vue]
<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>
```
references/recipes/auth.md
# 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.
```vue [pages/login.vue]
<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" />`
references/recipes/data-tables.md
# Data Tables
Complete patterns for displaying and managing tabular data.
## Basic table
```vue
<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)
```vue
<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.
```vue
<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:
```ts
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`.
```vue
<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.
```vue
<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:
```vue
<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 v8](https://tanstack.com/table/v8) — columns use `ColumnDef` format with `accessorKey`, `header`, `cell`
- Use `#<column>-cell` and `#<column>-header` template slots to customize rendering with Vue templates
- Alternatively, use the `h` function inside `header` and `cell` column properties for inline rendering
- Row data in slots is accessed via `row.original` (not `row` directly)
- Use `v-model:row-selection` for selection, `v-model:sorting` for sort state
- Wrap tables in `UDashboardPanel` with `#header` toolbar for the dashboard pattern
- For empty states, use the `#empty` slot
references/recipes/navigation.md
# 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.
```vue
<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](../layouts/landing.md).
## Sidebar navigation (dashboard)
See [dashboard layout](../layouts/dashboard.md) for the full sidebar pattern with `UDashboardSidebar` + `UNavigationMenu`. Key points:
- Pass `:collapsed="collapsed"` to `UNavigationMenu` inside collapsible sidebars
- Use `NavigationMenuItem[][]` (nested arrays) for separate nav groups
- Use `#footer` slot for user menu with `UDropdownMenu`
## Breadcrumbs
```vue
<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)
```vue
<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>
```
references/recipes/overlays.md
# Overlays
Patterns for modals, slideovers, drawers, and command palettes.
## Confirmation dialog
```vue
<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>
```
```ts
// 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
```vue
<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
```vue
<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)
```vue
<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>
```
SKILL.md
---
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/mcp
```
Key MCP tools:
- `search-components` — find components by name, category, or intent (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)
- `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](https://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](references/guidelines/design-system.md) — semantic colors, theming, brand customization, variants, the `ui` prop
- [component-selection](references/guidelines/component-selection.md) — decision matrices: when to use Modal vs Slideover, Select vs SelectMenu, Toast vs Alert, etc.
- [conventions](references/guidelines/conventions.md) — coding patterns, slot naming, items arrays, composables, keyboard shortcuts
- [forms](references/guidelines/forms.md) — form validation, field layout, error handling, Standard Schema
**Layouts** — full page structure patterns:
- [landing](references/layouts/landing.md) — landing pages, blog, changelog, pricing
- [dashboard](references/layouts/dashboard.md) — admin UI with sidebar and panels
- [docs](references/layouts/docs.md) — documentation sites with navigation and TOC
- [chat](references/layouts/chat.md) — AI chat with Vercel AI SDK
- [editor](references/layouts/editor.md) — rich text editor with toolbars
**Recipes** — complete patterns for common tasks:
- [data-tables](references/recipes/data-tables.md) — tables with filters, pagination, sorting, selection
- [auth](references/recipes/auth.md) — login, signup, forgot password forms
- [overlays](references/recipes/overlays.md) — modals, slideovers, drawers, command palette
- [navigation](references/recipes/navigation.md) — headers, sidebars, breadcrumbs, tabs
**Quick reference:**
- [components](references/components.md) — 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
```bash
pnpm add @nuxt/ui tailwindcss
```
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})
```
```css
/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
```
```vue
<!-- app.vue -->
<template>
<UApp>
<NuxtPage />
</UApp>
</template>
```
### Vue (Vite)
```bash
pnpm add @nuxt/ui tailwindcss
```
```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()
]
})
```
```ts
// 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')
```
```css
/* src/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
```
```vue
<!-- src/App.vue -->
<template>
<UApp>
<RouterView />
</UApp>
</template>
```
> Add `class="isolate"` to your root `<div id="app">` in `index.html`.
> For Inertia: use `ui({ router: 'inertia' })` in `vite.config.ts`.