This commit is contained in:
2026-09-27 11:51:31 +03:00
parent dfe330905c
commit ea5202f080
119 changed files with 79192 additions and 5 deletions
@@ -0,0 +1,130 @@
# 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
- Use `UDashboardSidebar` with `mode="drawer"` for a drawer-based mobile menu; `UModal` and `USlideover` don't expose a `mode` prop
- 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
@@ -0,0 +1,379 @@
# 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`, `UNavigationMenu`.
`UCommandPalette` uses a `groups` prop instead, with an `items` array on each group.
## 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
@@ -0,0 +1,384 @@
# 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;
}
```
@@ -0,0 +1,235 @@
# 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({ silent: true })
if (result) {
// valid — submit
}
}
async function validateEmail() {
await form.value?.validate({ name: 'email', silent: true })
}
function setServerError() {
form.value?.setErrors([
{ name: 'email', message: 'Email already taken' }
])
}
function resetErrors() {
form.value?.clear()
}
</script>
<template>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<!-- fields -->
</UForm>
</template>
```
By default, `validate()` throws a `FormValidationException` when validation fails. Pass `{ silent: true }` when you want it to return `false` instead. Use `clear()` to remove validation errors.
## 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>
```