Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
---
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 (Inertia), and AdonisJS (Inertia).
## 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/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
/* assets/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
```
```vue
<!-- src/App.vue -->
<template>
<UApp>
<RouterView />
</UApp>
</template>
```
> **Vue**: Add `class="isolate"` to your root `<div id="app">` in `index.html`.
> **Vue + Inertia**: Use `ui({ router: 'inertia' })` in `vite.config.ts`.
### UApp
Wrapping your app in `UApp` is **required** — it provides global config for toasts, tooltips, and programmatic overlays. It also accepts a `locale` prop for i18n (see [composables reference](references/composables.md)).
## Icons
Nuxt UI uses [Iconify](https://iconify.design/) for 200,000+ icons. In Nuxt, `@nuxt/icon` is auto-registered. In Vue, icons work out of the box via the Vite plugin.
### Naming convention
Icons use the format `i-{collection}-{name}`:
```vue
<UIcon name="i-lucide-sun" class="size-5" />
<UButton icon="i-lucide-plus" label="Add" />
<UAlert icon="i-lucide-info" title="Heads up" />
```
> Browse all icons at [icones.js.org](https://icones.js.org). The `lucide` collection is used throughout Nuxt UI defaults.
### Install icon collections locally
```bash
pnpm i @iconify-json/lucide
pnpm i @iconify-json/simple-icons
```
### Custom local collections (Nuxt)
```ts
// nuxt.config.ts
export default defineNuxtConfig({
icon: {
customCollections: [{
prefix: 'custom',
dir: './app/assets/icons'
}]
}
})
```
```vue
<UIcon name="i-custom-my-icon" />
```
## Theming & Branding
Nuxt UI ships with a default look. The goal is to adapt it to your brand so every app looks unique.
**Always use semantic utilities** (`text-default`, `bg-elevated`, `border-muted`), never raw Tailwind palette colors. See [references/theming.md](references/theming.md) for the full list.
### Colors
7 semantic colors (`primary`, `secondary`, `success`, `info`, `warning`, `error`, `neutral`) configurable at runtime:
```ts
// Nuxt — app.config.ts
export default defineAppConfig({
ui: { colors: { primary: 'indigo', neutral: 'zinc' } }
})
```
```ts
// Vue — vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: { colors: { primary: 'indigo', neutral: 'zinc' } }
})
]
})
```
### Customizing components
**Override priority** (highest wins): `ui` prop / `class` prop > global config > theme defaults.
The `ui` prop overrides a component's **slots** after variants are computed — it wins over everything:
```vue
<UButton :ui="{ base: 'rounded-none', trailingIcon: 'size-3 rotate-90' }" />
<UCard :ui="{ header: 'bg-muted', body: 'p-8' }" />
```
**Read the generated theme file** to find slot names for any component:
- **Nuxt**: `.nuxt/ui/<component>.ts`
- **Vue**: `node_modules/.nuxt-ui/ui/<component>.ts`
> For CSS variables, custom colors, global config, compound variants, and a **full brand customization playbook**, see [references/theming.md](references/theming.md)
## Composables
```ts
// Notifications
const toast = useToast()
toast.add({ title: 'Saved', color: 'success', icon: 'i-lucide-check' })
// Programmatic overlays
const overlay = useOverlay()
const modal = overlay.create(MyModal)
const { result } = modal.open({ title: 'Confirm' })
await result
// Keyboard shortcuts
defineShortcuts({
meta_k: () => openSearch(),
escape: () => close()
})
```
> For full composable reference, see [references/composables.md](references/composables.md)
## Form validation
Uses Standard Schema — works with Zod, Valibot, Yup, or Joi.
```vue
<script setup lang="ts">
import { z } from 'zod'
const schema = z.object({
email: z.string().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() {
// UForm validates before emitting @submit — state is valid here
}
</script>
<template>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormField name="email" label="Email" required>
<UInput v-model="state.email" type="email" />
</UFormField>
<UFormField name="password" label="Password" required>
<UInput v-model="state.password" type="password" />
</UFormField>
<UButton type="submit">Sign in</UButton>
</UForm>
</template>
```
> For all form components and validation patterns, see [references/components.md](references/components.md#form)
## Overlays
```vue
<!-- Modal -->
<UModal v-model:open="isOpen" title="Edit" description="Edit your profile">
<template #body>Content</template>
<template #footer>
<UButton variant="ghost" @click="isOpen = false">Cancel</UButton>
<UButton @click="save">Save</UButton>
</template>
</UModal>
<!-- Slideover (side panel) -->
<USlideover v-model:open="isOpen" title="Settings" side="right">
<template #body>Content</template>
</USlideover>
<!-- Dropdown menu (flat array) -->
<UDropdownMenu :items="[
{ label: 'Edit', icon: 'i-lucide-pencil' },
{ type: 'separator' },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
]">
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
</UDropdownMenu>
<!-- Dropdown menu (nested array groups with automatic separators) -->
<UDropdownMenu :items="[
[{ label: 'Edit', icon: 'i-lucide-pencil' }, { label: 'Duplicate', icon: 'i-lucide-copy' }],
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }]
]">
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
</UDropdownMenu>
```
> For all overlay components, see [references/components.md](references/components.md#overlay)
## Layouts
Nuxt UI provides components to compose full page layouts. Load the reference matching your use case:
| Layout | Description | Reference |
|---|---|---|
| Page | Landing, blog, changelog, pricing — public-facing pages | [layouts/page.md](references/layouts/page.md) |
| Dashboard | Admin UI with resizable sidebar and panels | [layouts/dashboard.md](references/layouts/dashboard.md) |
| Docs | Documentation with sidebar nav and TOC | [layouts/docs.md](references/layouts/docs.md) |
| Chat | AI chat with messages and prompt | [layouts/chat.md](references/layouts/chat.md) |
| Editor | Rich text editor with toolbars | [layouts/editor.md](references/layouts/editor.md) |
## Templates
Official starter templates at [github.com/nuxt-ui-templates](https://github.com/nuxt-ui-templates):
| Template | Framework | GitHub |
|---|---|---|
| Starter | Nuxt | [nuxt-ui-templates/starter](https://github.com/nuxt-ui-templates/starter) |
| Starter | Vue | [nuxt-ui-templates/starter-vue](https://github.com/nuxt-ui-templates/starter-vue) |
| Dashboard | Nuxt | [nuxt-ui-templates/dashboard](https://github.com/nuxt-ui-templates/dashboard) |
| Dashboard | Vue | [nuxt-ui-templates/dashboard-vue](https://github.com/nuxt-ui-templates/dashboard-vue) |
| SaaS | Nuxt | [nuxt-ui-templates/saas](https://github.com/nuxt-ui-templates/saas) |
| Landing | Nuxt | [nuxt-ui-templates/landing](https://github.com/nuxt-ui-templates/landing) |
| Docs | Nuxt | [nuxt-ui-templates/docs](https://github.com/nuxt-ui-templates/docs) |
| Portfolio | Nuxt | [nuxt-ui-templates/portfolio](https://github.com/nuxt-ui-templates/portfolio) |
| Chat | Nuxt | [nuxt-ui-templates/chat](https://github.com/nuxt-ui-templates/chat) |
| Editor | Nuxt | [nuxt-ui-templates/editor](https://github.com/nuxt-ui-templates/editor) |
| Changelog | Nuxt | [nuxt-ui-templates/changelog](https://github.com/nuxt-ui-templates/changelog) |
| Starter | Laravel | [nuxt-ui-templates/starter-laravel](https://github.com/nuxt-ui-templates/starter-laravel) |
| Starter | AdonisJS | [nuxt-ui-templates/starter-adonis](https://github.com/nuxt-ui-templates/starter-adonis) |
> When starting a new project, clone the matching template instead of setting up from scratch.
## Additional references
Load based on your task — **do not load all at once**:
- [references/theming.md](references/theming.md) — CSS variables, custom colors, component theme overrides
- [references/components.md](references/components.md) — all 125+ components by category with props and usage
- [references/composables.md](references/composables.md) — useToast, useOverlay, defineShortcuts
- Generated theme files — all slots, variants, and default classes for any component (Nuxt: `.nuxt/ui/<component>.ts`, Vue: `node_modules/.nuxt-ui/ui/<component>.ts`)

View File

@@ -0,0 +1,377 @@
# Components
125+ Vue components powered by Tailwind CSS and Reka UI. For any component's theme slots, read the generated theme file (Nuxt: `.nuxt/ui/<component>.ts`, Vue: `node_modules/.nuxt-ui/ui/<component>.ts`).
## Layout
Core structural components for organizing your application's layout.
| Component | Purpose |
|---|---|
| `UApp` | **Required** root wrapper for toasts, tooltips, overlays |
| `UHeader` | Responsive header with mobile menu (`#title`, `#default`, `#right`, `#body`) |
| `UFooter` | Footer (`#left`, `#default`, `#right`, `#top`, `#bottom`) |
| `UFooterColumns` | Multi-column footer with link groups |
| `UMain` | Main content area (respects `--ui-header-height`) |
| `UContainer` | Centered max-width container (`--ui-container`) |
## Element
Essential UI building blocks.
| Component | Key props |
|---|---|
| `UButton` | `label`, `icon`, `color`, `variant`, `size`, `loading`, `disabled`, `to` |
| `UBadge` | `label`, `color`, `variant`, `size` |
| `UAvatar` | `src`, `alt`, `icon`, `text`, `size` |
| `UAvatarGroup` | `max`, `size` — wraps multiple `UAvatar` |
| `UIcon` | `name`, `size` |
| `UCard` | `variant` — slots: `#header`, `#default`, `#footer` |
| `UAlert` | `title`, `description`, `icon`, `color`, `variant`, `close` |
| `UBanner` | `title`, `icon`, `close` — sticky top banner |
| `UChip` | `color`, `size`, `position` — notification dot on children |
| `UKbd` | `value` — keyboard key display |
| `USeparator` | `label`, `icon`, `orientation`, `type` |
| `USkeleton` | `class` — loading placeholder |
| `UProgress` | `value`, `max`, `color`, `size` |
| `UCalendar` | `v-model`, `range` (boolean), `multiple` (boolean) |
| `UCollapsible` | `v-model:open` — animated expand/collapse |
| `UFieldGroup` | Groups form inputs horizontally/vertically |
## Form
Comprehensive form components for user input.
| Component | Key props |
|---|---|
| `UInput` | `v-model`, `type`, `placeholder`, `icon`, `loading` |
| `UTextarea` | `v-model`, `rows`, `autoresize`, `maxrows` |
| `USelect` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `placeholder` |
| `USelectMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable`, `multiple` |
| `UInputMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable` — autocomplete |
| `UInputNumber` | `v-model`, `min`, `max`, `step` |
| `UInputDate` | `v-model`, `range` (boolean for range selection), `locale` |
| `UInputTime` | `v-model`, `hour-cycle` (12/24), `granularity` |
| `UInputTags` | `v-model`, `max`, `placeholder` |
| `UPinInput` | `v-model`, `length`, `type`, `mask` |
| `UCheckbox` | `v-model`, `label`, `description` |
| `UCheckboxGroup` | `v-model`, `items`, `orientation` |
| `URadioGroup` | `v-model`, `items`, `orientation` |
| `USwitch` | `v-model`, `label`, `on-icon`, `off-icon` |
| `USlider` | `v-model`, `min`, `max`, `step` |
| `UColorPicker` | `v-model`, `format` (hex/rgb/hsl/cmyk/lab), `size` |
| `UFileUpload` | `v-model`, `accept`, `multiple`, `variant` (area/button) |
| `UForm` | `schema`, `state`, `@submit` — validation wrapper |
| `UFormField` | `name`, `label`, `description`, `hint`, `required` |
### Form validation
Uses Standard Schema — works with Zod, Valibot, Yup, or Joi.
```vue
<script setup lang="ts">
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters')
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ email: '', password: '' })
const form = ref()
async function onSubmit() {
await form.value.validate()
}
</script>
<template>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<UFormField name="email" label="Email" required>
<UInput v-model="state.email" type="email" />
</UFormField>
<UFormField name="password" label="Password" required>
<UInput v-model="state.password" type="password" />
</UFormField>
<UButton type="submit">Submit</UButton>
</UForm>
</template>
```
With Valibot:
```vue
<script setup lang="ts">
import * as v from 'valibot'
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Min 8 characters'))
})
</script>
```
### File upload
```vue
<script setup>
const files = ref<File[]>([])
</script>
<template>
<UFileUpload v-model="files" accept="image/*" multiple>
<template #actions="{ open }">
<UButton label="Upload" icon="i-lucide-upload" color="neutral" variant="outline" @click="open()" />
</template>
</UFileUpload>
</template>
```
## Data
Components for displaying and organizing data.
| Component | Key props |
|---|---|
| `UTable` | `data`, `columns`, `loading`, `sticky` |
| `UAccordion` | `items`, `type` (single/multiple), `collapsible` |
| `UCarousel` | `items`, `orientation`, `arrows`, `dots` |
| `UTimeline` | `items` — vertical timeline |
| `UTree` | `items` — hierarchical tree |
| `UUser` | `name`, `description`, `avatar` — user display |
| `UEmpty` | `icon`, `title`, `description` — empty state |
| `UMarquee` | `repeat`, `reverse`, `orientation`, `pauseOnHover` — infinite scroll |
| `UScrollArea` | Custom scrollbar wrapper |
## Navigation
Components for user navigation and wayfinding.
| Component | Key props |
|---|---|
| `UNavigationMenu` | `items` (flat `T[]` or grouped `T[][]`), `orientation` (horizontal/vertical) |
| `UBreadcrumb` | `items` |
| `UTabs` | `items`, `orientation`, `variant` |
| `UStepper` | `items`, `orientation`, `color` |
| `UPagination` | `v-model`, `total`, `items-per-page` |
| `ULink` | `to`, `active`, `inactive` — styled NuxtLink |
| `UCommandPalette` | `v-model:open`, `groups` (`{ id, label, items }[]`), `placeholder` |
## Overlay
Floating UI elements that appear above the main content. **All require `<UApp>` wrapper.**
| Component | Key props |
|---|---|
| `UModal` | `v-model:open`, `title`, `description`, `fullscreen`, `scrollable` |
| `USlideover` | `v-model:open`, `title`, `side` (left/right/top/bottom) |
| `UDrawer` | `v-model:open`, `title`, `handle` |
| `UPopover` | `arrow`, `content: { side, align }`, `openDelay`, `closeDelay` |
| `UTooltip` | `text`, `content: { side }`, `delayDuration` |
| `UDropdownMenu` | `items` (flat `T[]` or grouped `T[][]` with separators, supports nested `children`) |
| `UContextMenu` | `items` (flat `T[]` or grouped `T[][]`) — right-click menu |
| `UToast` | Used via `useToast()` composable |
### Modal
```vue
<UModal v-model:open="isOpen" title="Edit" description="Edit your profile">
<template #body>Content</template>
<template #footer>
<UButton variant="ghost" @click="isOpen = false">Cancel</UButton>
<UButton @click="save">Save</UButton>
</template>
</UModal>
```
Slots: `#content`, `#header`, `#body`, `#footer`
### Slideover
```vue
<USlideover v-model:open="isOpen" title="Settings" side="right">
<template #body>Content</template>
</USlideover>
```
### Drawer
```vue
<UDrawer v-model:open="isOpen" title="Options" handle>
<template #body>Content</template>
</UDrawer>
```
### DropdownMenu
Items accept a flat array or a nested array (each sub-array is rendered as a group separated by dividers):
```vue
<!-- Flat array -->
<UDropdownMenu :items="[
{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit() },
{ type: 'separator' },
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
]">
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
</UDropdownMenu>
<!-- Nested array (groups with automatic separators) -->
<UDropdownMenu :items="[
[{ label: 'Edit', icon: 'i-lucide-pencil' }, { label: 'Duplicate', icon: 'i-lucide-copy' }],
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }]
]">
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
</UDropdownMenu>
```
### Toast
```ts
const toast = useToast()
toast.add({
title: 'Success',
description: 'Changes saved',
color: 'success',
icon: 'i-lucide-check-circle',
duration: 5000,
actions: [{ label: 'Undo', onClick: () => undo() }]
})
```
### Programmatic overlays
```ts
const overlay = useOverlay()
// create() returns a reusable instance
const confirmDialog = overlay.create(ConfirmDialog)
// open() returns an object with .result (a Promise)
const { result } = confirmDialog.open({
title: 'Delete?',
message: 'This cannot be undone.'
})
if (await result) {
// User confirmed
}
// Inside the overlay component, emit close with a value:
// emit('close', true) or emit('close', false)
```
### CommandPalette
```vue
<script setup>
const groups = [{
id: 'actions',
label: 'Actions',
items: [
{ label: 'New file', icon: 'i-lucide-file-plus', onSelect: () => {} },
{ label: 'Settings', to: '/settings' }
]
}]
defineShortcuts({ meta_k: () => { isOpen.value = true } })
</script>
<UCommandPalette v-model:open="isOpen" :groups="groups" placeholder="Search..." />
```
## Page
Pre-built sections for marketing and content pages.
| Component | Purpose |
|---|---|
| `UPage` | Multi-column grid (`#left`, `#default`, `#right`) |
| `UPageAside` | Sticky sidebar wrapper (visible from `lg`) |
| `UPageHero` | Hero section with title, description, links, media |
| `UPageSection` | Content section with headline, features grid |
| `UPageCTA` | Call to action block |
| `UPageHeader` | Page title and description |
| `UPageBody` | Main content area with prose styling |
| `UPageFeature` | Individual feature item |
| `UPageGrid` | Grid layout for cards |
| `UPageColumns` | Multi-column layout |
| `UPageCard` | Content card for grids |
| `UPageLogos` | Logo wall |
| `UPageAnchors` | Anchor links (simpler TOC) |
| `UPageLinks` | Related resource links |
| `UPageList` | List items |
| `UBlogPosts` | Responsive grid of blog posts (`orientation`) |
| `UBlogPost` | Individual blog post card |
| `UChangelogVersions` | Changelog version list |
| `UChangelogVersion` | Individual changelog entry |
| `UPricingPlans` | Pricing plan cards |
| `UPricingTable` | Feature comparison table |
## Dashboard
Specialized components for admin interfaces with resizable panels and sidebars.
| Component | Purpose |
|---|---|
| `UDashboardGroup` | Root wrapper — manages sidebar state |
| `UDashboardSidebar` | Resizable/collapsible sidebar (`#header`, `#default`, `#footer`) |
| `UDashboardPanel` | Content panel (`#header`, `#body`, `#footer`) |
| `UDashboardNavbar` | Panel navbar (`#left`, `#default`, `#right`) |
| `UDashboardToolbar` | Toolbar for filters/actions |
| `UDashboardSearch` | Command palette for dashboards |
| `UDashboardSearchButton` | Search trigger button |
| `UDashboardSidebarToggle` | Mobile sidebar toggle |
| `UDashboardSidebarCollapse` | Desktop collapse button |
| `UDashboardResizeHandle` | Custom resize handle |
## Chat
Components for conversational AI interfaces, powered by [Vercel AI SDK](https://ai-sdk.dev/).
| Component | Purpose |
|---|---|
| `UChatMessages` | Scrollable message list with auto-scroll |
| `UChatMessage` | Individual message display |
| `UChatPrompt` | Enhanced textarea for prompts |
| `UChatPromptSubmit` | Submit button with status handling |
| `UChatPalette` | Chat layout for overlays |
## Editor
Rich text editor powered by [TipTap](https://tiptap.dev/).
| Component | Purpose |
|---|---|
| `UEditor` | Editor (`v-model`, `content-type`: json/html/markdown) |
| `UEditorToolbar` | Toolbar (`layout`: fixed/bubble/floating) |
| `UEditorDragHandle` | Block drag-and-drop |
| `UEditorSuggestionMenu` | Slash command menu |
| `UEditorMentionMenu` | @ mention menu |
| `UEditorEmojiMenu` | Emoji picker |
## Content
Components integrating with `@nuxt/content`.
| Component | Purpose |
|---|---|
| `UContentNavigation` | Sidebar navigation tree |
| `UContentToc` | Table of contents |
| `UContentSurround` | Prev/next links |
| `UContentSearch` | Search command palette |
| `UContentSearchButton` | Search trigger button |
## Color Mode
| Component | Purpose |
|---|---|
| `UColorModeButton` | Toggle light/dark button |
| `UColorModeSwitch` | Toggle light/dark switch |
| `UColorModeSelect` | Dropdown selector |
| `UColorModeAvatar` | Avatar with different src per mode |
| `UColorModeImage` | Image with different src per mode |

View File

@@ -0,0 +1,127 @@
# Composables
## useToast
Show notifications. Requires `<UApp>` wrapper.
```ts
const toast = useToast()
toast.add({
title: 'Success',
description: 'Item saved',
color: 'success', // primary, success, error, warning, info
icon: 'i-lucide-check-circle',
duration: 5000, // 0 = never dismiss
actions: [{ label: 'Undo', onClick: () => {} }]
})
toast.remove('toast-id')
toast.clear()
```
## useOverlay
Programmatically create modals, slideovers, drawers.
```ts
const overlay = useOverlay()
// create() returns a reusable instance with open(), close(), patch()
const modal = overlay.create(MyComponent)
// open() accepts props and returns an object with .result (a Promise)
const { result } = modal.open({ title: 'Confirm' })
if (await result) {
// User confirmed
}
// Inside the overlay component, emit close with a value:
// emit('close', true) or emit('close', false)
// You can also close from outside:
modal.close(false)
```
## defineShortcuts
Define keyboard shortcuts.
```ts
defineShortcuts({
meta_k: () => openSearch(), // Cmd+K (Mac) / Ctrl+K (Win)
meta_shift_p: () => openPalette(), // Cmd+Shift+P
escape: () => close(),
ctrl_s: () => save(),
// With condition
meta_enter: {
handler: () => submit(),
whenever: [isFormValid]
}
})
```
| Key | Meaning |
|---|---|
| `meta` | Cmd (Mac) / Ctrl (Windows) |
| `ctrl` | Ctrl key |
| `alt` | Alt / Option key |
| `shift` | Shift key |
| `_` | Key separator |
## defineLocale / extendLocale
i18n locale definition.
```ts
import { fr } from '@nuxt/ui/locale'
// Use a built-in locale (50+ available)
// <UApp :locale="fr">
// Define custom locale
const locale = defineLocale({
name: 'Español',
code: 'es',
dir: 'ltr',
messages: {
select: { placeholder: 'Seleccionar...' }
}
})
// Extend existing locale
import { en } from '@nuxt/ui/locale'
const customEn = extendLocale(en, {
messages: { commandPalette: { placeholder: 'Search a component...' } }
})
```
```vue
<UApp :locale="fr"><NuxtPage /></UApp>
```
## extractShortcuts
Extract shortcut keys from a list of items (e.g., dropdown menu items) into a shortcuts map for `defineShortcuts`.
```ts
const items = [
{ label: 'New file', kbds: ['meta', 'n'], onSelect: () => newFile() },
{ label: 'Save', kbds: ['meta', 's'], onSelect: () => save() }
]
defineShortcuts(extractShortcuts(items))
```
## Quick reference
| Composable | Purpose |
|---|---|
| `useToast` | Show notifications |
| `useOverlay` | Programmatic modals/slideovers |
| `defineShortcuts` | Keyboard shortcuts |
| `defineLocale` / `extendLocale` | i18n locale |
| `extractShortcuts` | Parse shortcut definitions |

View File

@@ -0,0 +1,271 @@
# Chat Layout
Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration.
## Component tree
```
UApp
└── NuxtLayout (dashboard)
└── UDashboardGroup
├── UDashboardSidebar (conversations)
└── NuxtPage
└── UDashboardPanel
├── #header → UDashboardNavbar
├── #body → UContainer → UChatMessages
│ ├── #content → UChatReasoning, UChatTool, MDC
│ └── #indicator (loading)
└── #footer → UContainer → UChatPrompt
└── UChatPromptSubmit
```
## Setup
### Install AI SDK
```bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue
```
### Server endpoint
```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
}).toUIMessageStreamResponse()
})
```
## Full page chat
```vue [pages/chat/[id].vue]
<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'
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)"
>
<MDC
:value="part.text"
:cache-key="`reasoning-${message.id}-${index}`"
class="*:first:mt-0 *:last:mb-0"
/>
</UChatReasoning>
<UChatTool
v-else-if="isToolUIPart(part)"
:text="getToolName(part)"
:streaming="isToolStreaming(part)"
/>
<template v-else-if="isTextUIPart(part)">
<MDC
v-if="message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
</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
### ChatMessages
Scrollable message list with auto-scroll and loading indicator.
| Prop | Description |
|---|---|
| `messages` | Array of AI SDK messages |
| `status` | `'submitted'`, `'streaming'`, `'ready'`, `'error'` |
Slots: `#content` (receives `{ message }`), `#actions` (per-message), `#indicator` (loading)
### ChatMessage
Individual message bubble with avatar, actions, and slots.
| Prop | Description |
|---|---|
| `message` | AI SDK UIMessage object |
| `side` | `'left'` (default), `'right'` |
### ChatReasoning
Collapsible block for AI reasoning / thinking process. Auto-opens during streaming, auto-closes when done.
| Prop | Description |
|---|---|
| `text` | Reasoning text (displayed inside collapsible content) |
| `streaming` | Whether reasoning is actively streaming |
| `open` | Controlled open state |
Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai` to determine streaming state.
### ChatTool
Collapsible block for AI tool invocation status.
| Prop | Description |
|---|---|
| `text` | Tool status text (displayed in trigger) |
| `icon` | Icon name |
| `loading` | Show loading spinner on icon |
| `streaming` | Whether tool is actively running |
| `suffix` | Secondary text after label |
| `variant` | `'inline'` (default), `'card'` |
| `chevron` | `'trailing'` (default), `'leading'` |
Use `isToolStreaming(part)` from `@nuxt/ui/utils/ai` to determine if a tool is still running.
### ChatShimmer
Text shimmer animation for streaming states. Automatically used by ChatReasoning and ChatTool when streaming.
### ChatPrompt
Enhanced textarea form for prompts. Accepts all Textarea props.
| Prop | Description |
|---|---|
| `v-model` | Input text binding |
| `error` | Error from chat instance |
| `variant` | `'outline'` (default), `'subtle'`, `'soft'`, `'ghost'`, `'none'` |
Slots: `#default` (submit button), `#footer` (below input, e.g. model selector)
### ChatPromptSubmit
Submit button with automatic status handling (send/stop/reload).
### ChatPalette
Layout wrapper for chat inside overlays (Modal, Slideover, Drawer).
## Chat in a modal
```vue
<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
```vue
<script setup lang="ts">
const input = ref('')
const model = ref('claude-opus-4.6')
const models = [
{ label: 'Claude Opus 4.6', value: 'claude-opus-4.6', icon: 'i-simple-icons-anthropic' },
{ label: 'Gemini 3 Pro', value: 'gemini-3-pro', icon: 'i-simple-icons-googlegemini' },
{ label: 'GPT-5', value: 'gpt-5', icon: 'i-simple-icons-openai' }
]
</script>
<template>
<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>
</template>
```
## Conversation sidebar
```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>
```

View File

@@ -0,0 +1,220 @@
# Dashboard Layout
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
## 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
:items="items"
orientation="vertical"
:ui="{ link: collapsed ? 'justify-center' : undefined }"
/>
</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 #right>
<UButton icon="i-lucide-plus" label="New" />
</template>
</UDashboardNavbar>
</template>
<template #body>
<!-- Page content -->
</template>
</UDashboardPanel>
</template>
```
## Key components
### DashboardGroup
Root layout wrapper. Manages sidebar state and persistence.
| Prop | Default | Description |
|---|---|---|
| `storage` | `'cookie'` | State persistence: `'cookie'`, `'localStorage'`, `false` |
| `storage-key` | `'dashboard'` | Storage key name |
| `unit` | `'percentages'` | Size unit: `'percentages'` or `'pixels'` |
### DashboardSidebar
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
| Prop | Default | Description |
|---|---|---|
| `resizable` | `false` | Enable resize by dragging |
| `collapsible` | `false` | Enable collapse when dragged to edge |
| `side` | `'left'` | `'left'` or `'right'` |
| `mode` | `'slideover'` | Mobile menu mode: `'modal'`, `'slideover'`, `'drawer'` |
Slots receive `{ collapsed }` prop. Control state: `v-model:collapsed`, `v-model:open` (mobile).
### DashboardPanel
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw) slots.
| Prop | Default | Description |
|---|---|---|
| `id` | `` | Unique ID (required for multi-panel) |
| `resizable` | `false` | Enable resize by dragging |
### DashboardNavbar / DashboardToolbar
Navbar has `#left`, `#default`, `#right` slots and a `title` prop. Toolbar has the same slots for filters/actions below the navbar.
## 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>
```

View File

@@ -0,0 +1,141 @@
# Docs Layout
Build documentation sites with sidebar navigation, table of contents, and surround links.
> Requires `@nuxt/content` module for navigation, search, and TOC.
## 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>
```
> The outer `UPage` in the layout handles the left sidebar. The inner `UPage` in the page handles the right sidebar. They nest correctly.
## Key components
- `UPage` — Multi-column grid layout with `#left`, `#default`, `#right` slots
- `UPageAside` — Sticky sidebar wrapper (visible from `lg` breakpoint)
- `UPageHeader` — Page title and description
- `UPageBody` — Main content area
- `UContentNavigation` — Sidebar navigation tree
- `UContentToc` — Table of contents
- `UContentSurround` — Prev/next links
- `UContentSearch` / `UContentSearchButton` — Search command palette
- `UPageAnchors` — Simpler alternative to full TOC
- `UPageLinks` — Related resource links

View File

@@ -0,0 +1,168 @@
# Editor Layout
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
## Component tree
```
UApp
├── UHeader
├── UMain
│ └── NuxtPage
│ └── UContainer
│ └── UEditor
│ ├── UEditorToolbar (fixed / bubble / floating)
│ ├── UEditorDragHandle
│ ├── UEditorSuggestionMenu
│ ├── UEditorMentionMenu
│ └── UEditorEmojiMenu
└── UFooter
```
## Page
```vue [pages/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>
<UPage>
<UPageHeader title="Editor">
<template #actions>
<UButton label="Save" icon="i-lucide-save" />
</template>
</UPageHeader>
<UPageBody>
<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>
</UPageBody>
</UPage>
</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, HTML, or markdown via `content-type` prop)
- `UEditorToolbar` — Toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines)
- `UEditorDragHandle` — Block drag-and-drop handle
- `UEditorSuggestionMenu` — Slash command menu
- `UEditorMentionMenu` — @ mention menu
- `UEditorEmojiMenu` — Emoji picker
## Toolbar modes
```vue
<!-- Fixed (default) -->
<UEditor v-model="content">
<UEditorToolbar />
</UEditor>
<!-- Bubble (appears on text selection) -->
<UEditor v-model="content">
<UEditorToolbar layout="bubble" />
</UEditor>
<!-- Floating (appears on empty lines) -->
<UEditor v-model="content">
<UEditorToolbar layout="floating" />
</UEditor>
```
## 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 components for a multi-document editor with a sidebar.
```vue [layouts/editor.vue]
<template>
<UDashboardGroup>
<UDashboardSidebar collapsible resizable>
<template #header>
<UButton icon="i-lucide-plus" label="New document" block />
</template>
<template #default>
<UNavigationMenu
: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>
```

View File

@@ -0,0 +1,260 @@
# Page Layout
Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components.
## 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>
```
## 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>
```
## 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 from our team." />
<UPageBody>
<UContainer>
<UBlogPosts>
<UBlogPost
v-for="(post, index) in posts"
:key="index"
v-bind="post"
:to="post.path"
/>
</UBlogPosts>
</UContainer>
</UPageBody>
</UPage>
</template>
```
## Blog article
```vue [pages/blog/[slug].vue]
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(route.path, () => {
return queryCollection('posts').path(route.path).first()
})
</script>
<template>
<UPage>
<UPageHeader :title="post.title" :description="post.description" />
<UPageBody>
<ContentRenderer :value="post" />
</UPageBody>
<template #right>
<UContentToc :links="post.body.toc.links" />
</template>
</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, index) in versions" :key="index" v-bind="version" />
</UChangelogVersions>
</UContainer>
</UPageBody>
</UPage>
</template>
```
## Key components
### Page sections
- `UPageHero` — Hero with title, description, links, and optional media (`orientation`: horizontal/vertical)
- `UPageSection` — Content section with headline, title, description, and `features` grid
- `UPageCTA` — Call to action block
- `UPageHeader` — Page title and description
- `UPageBody` — Main content area with prose styling
### Grids & cards
- `UPageGrid` / `UPageColumns` — Grid layouts
- `UPageCard` — Content card for grids
- `UPageFeature` — Individual feature item
- `UPageLogos` — Logo wall
### Blog & changelog
- `UBlogPosts` — Responsive grid of posts (`orientation`: horizontal/vertical)
- `UBlogPost` — Individual post card
- `UChangelogVersions` / `UChangelogVersion` — Changelog entries
### Pricing
- `UPricingPlans` — Pricing plan cards
- `UPricingTable` — Feature comparison table
### Footer
- `UFooterColumns` — Multi-column footer with link groups
## Variations
### Alternating 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>
```
### Feature grid
```vue
<UPageSection headline="Features" title="Why choose us">
<UPageGrid>
<UPageCard v-for="feature in features" :key="feature.title" v-bind="feature" />
</UPageGrid>
</UPageSection>
```
### Blog with sidebar
```vue [layouts/blog.vue]
<template>
<UPage>
<template #left>
<UPageAside>
<UNavigationMenu
:items="[
{ label: 'All posts', to: '/blog', icon: 'i-lucide-newspaper' },
{ label: 'Tutorials', to: '/blog/tutorials', icon: 'i-lucide-graduation-cap' },
{ label: 'Announcements', to: '/blog/announcements', icon: 'i-lucide-megaphone' }
]"
orientation="vertical"
/>
</UPageAside>
</template>
<slot />
</UPage>
</template>
```

View File

@@ -0,0 +1,427 @@
# Theming
## Semantic colors
| Color | Default | Purpose |
|---|---|---|
| `primary` | green | CTAs, active states, brand |
| `secondary` | blue | Secondary actions |
| `success` | green | Success messages |
| `info` | blue | Informational |
| `warning` | yellow | Warnings |
| `error` | red | Errors, destructive actions |
| `neutral` | slate | Text, borders, disabled |
## 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' }
}
})
```
You can only use colors that exist in your theme — either [Tailwind's default colors](https://tailwindcss.com/docs/colors) or custom colors defined with `@theme`.
## Adding custom colors
1. Define all 11 shades in CSS:
```css
/* 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 as a semantic color value: `ui: { colors: { primary: 'brand' } }`
You can only use colors that have all shades defined — either from Tailwind's defaults or custom `@theme` definitions.
### Extending with new semantic color names
If you need a new semantic color beyond the defaults (e.g., `tertiary`), register it in `theme.colors`:
```ts
// Nuxt — nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
}
}
})
```
```ts
// Vue — vite.config.ts
ui({
theme: {
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
}
})
```
Then assign it: `ui: { colors: { tertiary: 'indigo' } }` and use it via the `color` prop: `<UButton color="tertiary">`.
## CSS utilities
### Text
| Class | Use | Light value | Dark value |
|---|---|---|---|
| `text-default` | Body text | `neutral-700` | `neutral-200` |
| `text-muted` | Secondary text | `neutral-500` | `neutral-400` |
| `text-dimmed` | Placeholders, hints | `neutral-400` | `neutral-500` |
| `text-toned` | Subtitles | `neutral-600` | `neutral-300` |
| `text-highlighted` | Headings, emphasis | `neutral-900` | `white` |
| `text-inverted` | On dark/light backgrounds | `white` | `neutral-900` |
### Background
| Class | Use | Light value | Dark value |
|---|---|---|---|
| `bg-default` | Page background | `white` | `neutral-900` |
| `bg-muted` | Subtle sections | `neutral-50` | `neutral-800` |
| `bg-elevated` | Cards, modals | `neutral-100` | `neutral-800` |
| `bg-accented` | Hover states | `neutral-200` | `neutral-700` |
| `bg-inverted` | Inverted sections | `neutral-900` | `white` |
### Border
| Class | Use | Light value | Dark value |
|---|---|---|---|
| `border-default` | Default borders | `neutral-200` | `neutral-800` |
| `border-muted` | Subtle borders | `neutral-200` | `neutral-700` |
| `border-accented` | Emphasized borders | `neutral-300` | `neutral-700` |
| `border-inverted` | Inverted borders | `neutral-900` | `white` |
### Semantic color utilities
Each semantic color (`primary`, `secondary`, `success`, `info`, `warning`, `error`) is available as a Tailwind utility: `text-primary`, `bg-primary`, `border-primary`, `ring-primary`, etc.
They resolve to shade **500** in light mode and shade **400** in dark mode (via `--ui-<color>` CSS variables). This is generated at runtime by the colors plugin — you don't need to write dark-mode variants manually.
To adjust which shade is used, override `--ui-primary` (or any semantic color) in your `main.css`:
```css
:root { --ui-primary: var(--ui-color-primary-600); }
.dark { --ui-primary: var(--ui-color-primary-300); }
```
### CSS variables
All customizable in `main.css`:
```css
:root {
--ui-radius: 0.25rem; /* base radius for all components */
--ui-container: 80rem; /* UContainer max-width */
--ui-header-height: 4rem; /* UHeader height */
--ui-primary: var(--ui-color-primary-500); /* adjust shade used */
}
.dark {
--ui-primary: var(--ui-color-primary-400);
}
```
### Solid colors (black/white)
```css
:root { --ui-primary: black; }
.dark { --ui-primary: white; }
```
## Component theme customization
### How it works
Components are styled with [Tailwind Variants](https://www.tailwind-variants.org/). The theme defines:
- **`slots`** — named style targets (e.g., `root`, `base`, `label`, `leadingIcon`)
- **`variants`** — styles applied based on props (e.g., `color`, `variant`, `size`)
- **`compoundVariants`** — styles for specific prop combinations (e.g., `color: 'primary'` + `variant: 'outline'`)
- **`defaultVariants`** — default prop values when none are specified
### Override priority
**`ui` prop / `class` prop > global config > theme defaults**
The `ui` prop overrides slots **after** variants are computed. If the `size: 'md'` variant applies `size-5` to `trailingIcon`, and you set `:ui="{ trailingIcon: 'size-3' }"`, the `size-3` wins.
Tailwind Variants uses [tailwind-merge](https://github.com/dcastil/tailwind-merge) under the hood so conflicting classes are resolved automatically.
### Understanding the generated theme
Every component's full resolved theme is generated at build time. Always read this file before customizing a component — it shows exactly what classes are applied where.
- **Nuxt**: `.nuxt/ui/<component>.ts`
- **Vue**: `node_modules/.nuxt-ui/ui/<component>.ts`
For example, the card theme:
```ts
{
slots: {
root: "rounded-lg overflow-hidden",
header: "p-4 sm:px-6",
body: "p-4 sm:p-6",
footer: "p-4 sm:px-6"
},
variants: {
variant: {
outline: { root: "bg-default ring ring-default divide-y divide-default" },
soft: { root: "bg-elevated/50 divide-y divide-default" }
}
},
defaultVariants: { variant: "outline" }
}
```
### Global config
Override the theme for all instances of a component:
```ts
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
button: {
slots: {
base: 'font-bold rounded-full'
},
variants: {
size: {
md: { leadingIcon: 'size-4' }
}
},
compoundVariants: [{
color: 'neutral',
variant: 'outline',
class: { base: 'ring-2' }
}],
defaultVariants: {
color: 'neutral',
variant: 'outline'
}
}
}
})
```
```ts
// Vue — vite.config.ts
ui({
ui: {
button: {
slots: { base: 'font-bold rounded-full' },
defaultVariants: { color: 'neutral', variant: 'outline' }
}
}
})
```
### Per-instance (`ui` prop)
Overrides slots after variant computation:
```vue
<UButton :ui="{ base: 'font-mono', trailingIcon: 'size-3 rotate-90' }" />
<UCard :ui="{ root: 'shadow-xl', body: 'p-8' }" />
```
### Per-instance (`class` prop)
Overrides the `root` or `base` slot:
```vue
<UButton class="rounded-none">Square</UButton>
```
Components without slots (e.g., `UContainer`, `USkeleton`, `UMain`) only have the `class` prop.
### Theme structure patterns
**Slots-based** (most components — `slots` is an object in the generated theme):
```ts
// global config
ui: {
button: {
slots: { base: 'font-bold' }
}
}
// per instance
<UButton :ui="{ base: 'font-bold' }" />
```
**Flat base** (`base` is a top-level string in the generated theme):
```ts
// global config
ui: {
container: {
base: 'max-w-lg'
}
}
// per instance — class prop only
<UContainer class="max-w-lg" />
```
Always check the generated theme file to see which pattern applies.
## Dark mode
```ts
const colorMode = useColorMode()
colorMode.preference = 'dark' // 'light', 'dark', 'system'
```
```vue
<UColorModeButton /> <!-- Toggle -->
<UColorModeSelect /> <!-- Dropdown -->
```
## Fonts
```css
/* assets/css/main.css */
@theme {
--font-sans: 'Public Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
```
In Nuxt, fonts defined with `@theme` are automatically loaded by the `@nuxt/fonts` module.
## Brand customization playbook
Follow these steps to fully rebrand Nuxt UI (e.g., "make a Ghibli theme", "match our corporate brand"):
### Step 1 — Define the color palette
Pick colors that match the brand. Map them to semantic roles:
```ts
// app.config.ts (Nuxt) or vite.config.ts (Vue)
ui: {
colors: {
primary: 'emerald', // brand accent
secondary: 'amber', // secondary accent
success: 'green',
info: 'sky',
warning: 'orange',
error: 'rose',
neutral: 'stone' // affects all text, borders, backgrounds
}
}
```
If no Tailwind default color fits, define custom shades in CSS (see [Adding custom colors](#adding-custom-colors)):
```css
@theme static {
--color-forest-50: #f0fdf4;
/* ... all 11 shades (50950) ... */
--color-forest-950: #052e16;
}
```
Then use it: `primary: 'forest'`.
### Step 2 — Set fonts
```css
/* assets/css/main.css */
@theme {
--font-sans: 'Quicksand', system-ui, sans-serif;
}
```
### Step 3 — Adjust CSS variables
```css
:root {
--ui-radius: 0.75rem; /* rounder = softer/playful, smaller = sharper/corporate */
--ui-primary: var(--ui-color-primary-600); /* adjust which shade is used */
}
.dark {
--ui-primary: var(--ui-color-primary-400);
}
```
### Step 4 — Override key components globally
Read the generated theme files to find slot names, then apply global overrides:
```ts
// app.config.ts (Nuxt) or vite.config.ts (Vue)
ui: {
// ... colors from Step 1
button: {
slots: {
base: 'rounded-full font-semibold'
},
defaultVariants: {
variant: 'soft'
}
},
card: {
slots: {
root: 'rounded-2xl shadow-lg'
}
},
badge: {
slots: {
base: 'rounded-full'
}
}
}
```
> **Tip**: Read `.nuxt/ui/button.ts` (Nuxt) or `node_modules/.nuxt-ui/ui/button.ts` (Vue) to see all available slots and variants before overriding.
### Step 5 — Verify dark mode
Check that both modes look correct. Adjust `--ui-primary` shade per mode and test contrast. Use `useColorMode()` to toggle during development.
### Quick checklist
| Step | What to change | Where |
|---|---|---|
| Colors | `primary`, `secondary`, `neutral` | `app.config.ts` / `vite.config.ts` |
| Custom palette | 11 shades per color | `main.css` (`@theme static`) |
| Fonts | `--font-sans`, `--font-mono` | `main.css` (`@theme`) |
| Radius | `--ui-radius` | `main.css` (`:root`) |
| Primary shade | `--ui-primary` | `main.css` (`:root` + `.dark`) |
| Component shapes | Global slot overrides | `app.config.ts` / `vite.config.ts` |
| Dark mode | Verify contrast, adjust variables | `main.css` (`.dark`) |