nvim
This commit is contained in:
@@ -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>
|
||||
```
|
||||
Reference in New Issue
Block a user