nvim
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# Components
|
||||
|
||||
Quick-reference index of all 125+ components. For full API docs (props, slots, events, examples), use the MCP `get-component` or `get-component-metadata` tools.
|
||||
|
||||
## Layout
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------- | ------------------------------------------------------------ |
|
||||
| `UApp` | **Required** root wrapper — toasts, tooltips, overlays, i18n |
|
||||
| `UHeader` | Responsive header with mobile menu |
|
||||
| `UFooter` | Footer with left/right/top/bottom slots |
|
||||
| `UFooterColumns` | Multi-column footer with link groups |
|
||||
| `UMain` | Main content area |
|
||||
| `UContainer` | Centered max-width container |
|
||||
| `USplitter` | Resizable panels separated by draggable handles |
|
||||
| `ULink` | Enhanced link — NuxtLink/RouterLink with active states |
|
||||
|
||||
## Element
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------- | ---------------------------------------------------- |
|
||||
| `UButton` | Buttons — links, actions, icons, loading states |
|
||||
| `UBadge` | Labels, tags, status indicators |
|
||||
| `UAvatar` | User photos, initials, icons |
|
||||
| `UAvatarGroup` | Stacked avatars with `max` limit |
|
||||
| `UIcon` | Iconify icons (`i-{collection}-{name}`) |
|
||||
| `UCard` | Bordered container with header/body/footer |
|
||||
| `UAlert` | Inline messages — info, warning, error, success |
|
||||
| `UBanner` | App-wide sticky announcement bar |
|
||||
| `UChip` | Notification dot overlay on children |
|
||||
| `UKbd` | Keyboard key display |
|
||||
| `USeparator` | Divider line with optional label |
|
||||
| `USkeleton` | Loading placeholder |
|
||||
| `UProgress` | Progress bar |
|
||||
| `UProgressGroup` | Segmented progress bar with a list of items |
|
||||
| `UToast` | Toast notification (shown via `useToast`) |
|
||||
| `UCalendar` | Date calendar (single, range, multiple) |
|
||||
| `UCollapsible` | Animated expand/collapse |
|
||||
| `UFieldGroup` | Group form inputs horizontally |
|
||||
| `UMarquee` | Scrolling content ticker |
|
||||
| `UCarousel` | Image/content carousel with autoplay |
|
||||
| `UEmpty` | Empty state placeholder with icon, title, actions |
|
||||
| `UError` | Error display with retry action |
|
||||
| `UScrollArea` | Scrollable area with custom scrollbar |
|
||||
| `UTimeline` | Timeline display for events and activity |
|
||||
| `UUser` | User display — avatar + name + description |
|
||||
| `UTheme` | Theme provider — scoped color overrides for children |
|
||||
|
||||
## Form
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------- | ---------------------------------------------- |
|
||||
| `UAuthForm` | Pre-built auth form with social providers |
|
||||
| `UInput` | Text input — text, email, password, search |
|
||||
| `UTextarea` | Multi-line text with autoresize |
|
||||
| `USelect` | Native-like dropdown for small lists |
|
||||
| `USelectMenu` | Rich searchable dropdown, multi-select, groups |
|
||||
| `UInputMenu` | Autocomplete / combobox |
|
||||
| `UInputNumber` | Numeric input with +/- controls |
|
||||
| `UInputDate` | Date picker with calendar |
|
||||
| `UInputTime` | Time picker (12/24h) |
|
||||
| `UInputTags` | Tag/chip input |
|
||||
| `UPinInput` | Verification code input |
|
||||
| `UCheckbox` | Single boolean checkbox |
|
||||
| `UCheckboxGroup` | Multiple checkboxes |
|
||||
| `URadioGroup` | Radio button group |
|
||||
| `USwitch` | Toggle switch |
|
||||
| `USlider` | Range slider |
|
||||
| `UColorPicker` | Color picker (hex/rgb/hsl) |
|
||||
| `UFileUpload` | File upload (button or drop area) |
|
||||
| `UForm` | Validation wrapper with Standard Schema |
|
||||
| `UFormField` | Field wrapper with label, hint, errors |
|
||||
|
||||
## Overlay
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------- | -------------------------------------- |
|
||||
| `UModal` | Centered dialog — confirmations, forms |
|
||||
| `USlideover` | Side panel — details, editing |
|
||||
| `UDrawer` | Bottom sheet — mobile actions |
|
||||
| `UPopover` | Contextual popup attached to trigger |
|
||||
| `UTooltip` | Hover/focus hint (non-interactive) |
|
||||
| `UContextMenu` | Right-click menu |
|
||||
| `UCommandPalette` | Search + keyboard navigation (Cmd+K) |
|
||||
|
||||
## Navigation
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------- | ------------------------------------------ |
|
||||
| `USidebar` | Standalone sidebar with header/body/footer |
|
||||
| `UNavigationMenu` | Primary nav — horizontal or vertical |
|
||||
| `UTabs` | Tab switcher within a page |
|
||||
| `UBreadcrumb` | Location hierarchy |
|
||||
| `UDropdownMenu` | Action menu on a trigger |
|
||||
| `UPagination` | Page navigation |
|
||||
| `UStepper` | Multi-step wizard |
|
||||
| `UAccordion` | Collapsible sections |
|
||||
|
||||
## Data
|
||||
|
||||
| Component | Purpose |
|
||||
| --------- | ------------------------------------------------------------ |
|
||||
| `UTable` | Data table (TanStack Table) with sorting, selection, pinning |
|
||||
| `UTree` | Hierarchical tree view |
|
||||
|
||||
## Dashboard
|
||||
|
||||
| Component | Purpose |
|
||||
| --------------------------- | ------------------------------------- |
|
||||
| `UDashboardGroup` | Root dashboard wrapper |
|
||||
| `UDashboardSidebar` | Resizable, collapsible sidebar |
|
||||
| `UDashboardPanel` | Content panel with header/body/footer |
|
||||
| `UDashboardNavbar` | Panel header bar |
|
||||
| `UDashboardToolbar` | Filter/action bar below navbar |
|
||||
| `UDashboardResizeHandle` | Resize handle between panels |
|
||||
| `UDashboardSidebarToggle` | Mobile sidebar toggle button |
|
||||
| `UDashboardSearchButton` | Search button for sidebar |
|
||||
| `UDashboardSearch` | Dashboard-level search overlay |
|
||||
| `UDashboardSidebarCollapse` | Collapse button for sidebar |
|
||||
|
||||
## Page (marketing)
|
||||
|
||||
| Component | Purpose |
|
||||
| -------------- | ----------------------------------------------- |
|
||||
| `UPage` | Multi-column layout with left/right sidebars |
|
||||
| `UPageHero` | Hero section — title, description, links, media |
|
||||
| `UPageSection` | Content section with features grid |
|
||||
| `UPageCTA` | Call to action block |
|
||||
| `UPageHeader` | Page title and description |
|
||||
| `UPageBody` | Main content area |
|
||||
| `UPageGrid` | Card grid layout |
|
||||
| `UPageColumns` | Multi-column layout |
|
||||
| `UPageCard` | Content card for grids |
|
||||
| `UPageFeature` | Feature item |
|
||||
| `UPageLogos` | Logo cloud |
|
||||
| `UPageAside` | Sticky sidebar wrapper |
|
||||
| `UPageAnchors` | Simple anchor links |
|
||||
| `UPageLinks` | Related resource links |
|
||||
| `UPageList` | List layout for page items |
|
||||
|
||||
## Blog & Changelog
|
||||
|
||||
| Component | Purpose |
|
||||
| -------------------- | -------------------------- |
|
||||
| `UBlogPosts` | Blog post grid |
|
||||
| `UBlogPost` | Individual post card |
|
||||
| `UChangelogVersions` | Changelog list |
|
||||
| `UChangelogVersion` | Individual changelog entry |
|
||||
|
||||
## Pricing
|
||||
|
||||
| Component | Purpose |
|
||||
| --------------- | ---------------------------- |
|
||||
| `UPricingPlans` | Pricing plan cards |
|
||||
| `UPricingPlan` | Individual pricing plan card |
|
||||
| `UPricingTable` | Feature comparison table |
|
||||
|
||||
## Prose — Base Typography
|
||||
|
||||
Standard Markdown elements auto-resolved by Comark/Content/MDC. No `::` prefix needed — they map directly from markdown syntax (`# Heading` → `ProseH1`, `**bold**` → `ProseStrong`, etc.). Themed via `appConfig.ui.prose.<name>`.
|
||||
|
||||
| Component | Renders | Notable |
|
||||
| -------------------------------------- | --------------- | ----------------------------------------------------------------- |
|
||||
| `H1` `H2` `H3` `H4` | Headings | H1–H3 get anchor links + TOC entries |
|
||||
| `P` | Paragraph | |
|
||||
| `A` | Link | External links get target/rel handling |
|
||||
| `Strong` | Bold | |
|
||||
| `Em` | Italic | |
|
||||
| `Blockquote` | Blockquote | |
|
||||
| `Hr` | Horizontal rule | |
|
||||
| `Ul` `Ol` `Li` | Lists | Supports nesting and mixed lists |
|
||||
| `Table` `Thead` `Tbody` `Tr` `Th` `Td` | Tables | |
|
||||
| `Img` | Image | Zoom on click (`:zoom="false"` to disable), `@nuxt/image` support |
|
||||
| `Pre` | Code block | Copy button, filename + icon, line highlighting (`{2,4-6}`), diff |
|
||||
| `Code` | Inline code | `color` and `lang` props |
|
||||
|
||||
## Prose — Feature Components
|
||||
|
||||
Nuxt UI-specific Prose components. In markdown files they are used **without the `Prose` prefix** (e.g. `::callout`, `::steps`). In Vue they are referenced as `ProseCallout`, `ProseSteps`, etc. Comark resolves them automatically when `@nuxt/ui` is installed.
|
||||
|
||||
Nuxt UI also registers shorthand aliases for `Callout`: `::note`, `::tip`, `::warning`, `::caution` (preset `color` + `icon`).
|
||||
|
||||
| Component | Purpose |
|
||||
| --------------------------- | --------------------------------------------------------------------- |
|
||||
| `Callout` | Highlighted note/warning/tip (`color`, `icon`, `to`) |
|
||||
| `Badge` | Inline badge/tag |
|
||||
| `Kbd` | Keyboard key |
|
||||
| `Icon` | Inline Iconify icon |
|
||||
| `Prompt` | Terminal prompt block |
|
||||
| `Card` `CardGroup` | Content card and card grid |
|
||||
| `Steps` | Numbered step list (`level` prop sets heading depth) |
|
||||
| `Tabs` `TabsItem` | Tabbed content (`sync` for localStorage, `hash` for scroll-on-change) |
|
||||
| `Accordion` `AccordionItem` | Collapsible accordion sections |
|
||||
| `Collapsible` | Single collapsible section |
|
||||
| `Field` `FieldGroup` | Form field display |
|
||||
| `CodeGroup` | Tabbed code blocks |
|
||||
| `CodeCollapse` | Collapsible code block |
|
||||
| `CodeIcon` | File-type icon in code headers |
|
||||
| `CodePreview` | Code + live rendered preview side by side |
|
||||
| `CodeTree` | File tree display |
|
||||
| `Script` | Script injection |
|
||||
|
||||
## Content (Nuxt Content)
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------------- | ------------------------------- |
|
||||
| `UContentNavigation` | Sidebar navigation from content |
|
||||
| `UContentToc` | Table of contents |
|
||||
| `UContentSurround` | Prev/next navigation |
|
||||
| `UContentSearch` | Search command palette |
|
||||
| `UContentSearchButton` | Trigger for content search |
|
||||
|
||||
## Chat (AI)
|
||||
|
||||
| Component | Purpose |
|
||||
| ------------------- | ------------------------------ |
|
||||
| `UChatMessages` | Scrollable message list |
|
||||
| `UChatMessage` | Individual message bubble |
|
||||
| `UChatReasoning` | Collapsible AI reasoning block |
|
||||
| `UChatTool` | Tool invocation status |
|
||||
| `UChatShimmer` | Streaming text animation |
|
||||
| `UChatPrompt` | Enhanced textarea for prompts |
|
||||
| `UChatPromptSubmit` | Submit button with status |
|
||||
| `UChatPalette` | Chat layout for overlays |
|
||||
|
||||
## Editor
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `UEditor` | Rich text editor (JSON/HTML/Markdown) |
|
||||
| `UEditorToolbar` | Toolbar (fixed/bubble/floating) |
|
||||
| `UEditorDragHandle` | Block drag-and-drop |
|
||||
| `UEditorSuggestionMenu` | Slash command menu |
|
||||
| `UEditorMentionMenu` | @ mention menu |
|
||||
| `UEditorEmojiMenu` | Emoji picker |
|
||||
|
||||
## Color Mode
|
||||
|
||||
| Component | Purpose |
|
||||
| ------------------ | ----------------------------------- |
|
||||
| `UColorModeButton` | Toggle button (light/dark) |
|
||||
| `UColorModeSwitch` | Toggle switch (light/dark) |
|
||||
| `UColorModeSelect` | Dropdown (light/dark/system) |
|
||||
| `UColorModeAvatar` | Avatar that changes with color mode |
|
||||
| `UColorModeImage` | Image that changes with color mode |
|
||||
@@ -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>
|
||||
```
|
||||
@@ -0,0 +1,263 @@
|
||||
# Chat Layout
|
||||
|
||||
Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration.
|
||||
|
||||
## When to use
|
||||
|
||||
- AI chatbot interfaces
|
||||
- Customer support chat
|
||||
- Any conversational UI with streaming responses
|
||||
|
||||
## Setup
|
||||
|
||||
### Install dependencies
|
||||
|
||||
**Nuxt:**
|
||||
|
||||
```bash
|
||||
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
|
||||
```
|
||||
|
||||
**Vue (Vite):**
|
||||
|
||||
```bash
|
||||
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
|
||||
```
|
||||
|
||||
### Register Comark module
|
||||
|
||||
**Nuxt:**
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@comark/nuxt'
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**Vue (Vite):** No module registration needed, import directly from `@comark/vue`.
|
||||
|
||||
> `@comark/nuxt` (or `@comark/vue` for Vue projects) provides the `Comark` component used to render AI responses as streaming Markdown, it incrementally renders tokens as they arrive and automatically enables Nuxt UI's prose components.
|
||||
|
||||
### Dark mode for syntax highlighting
|
||||
|
||||
When using the `highlight` plugin, add the following CSS to your stylesheet:
|
||||
|
||||
```css [main.css]
|
||||
html.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
```
|
||||
|
||||
### Server endpoint
|
||||
|
||||
Using [Vercel AI Gateway](https://vercel.com/ai-gateway) (recommended):
|
||||
|
||||
```ts [server/api/chat.post.ts]
|
||||
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
|
||||
import { gateway } from '@ai-sdk/gateway'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { messages } = await readBody(event)
|
||||
|
||||
const result = streamText({
|
||||
model: gateway('anthropic/claude-sonnet-5'),
|
||||
instructions: 'You are a helpful assistant.',
|
||||
messages: await convertToModelMessages(messages)
|
||||
})
|
||||
|
||||
const stream = toUIMessageStream({ stream: result.stream })
|
||||
return createUIMessageStreamResponse({ stream })
|
||||
})
|
||||
```
|
||||
|
||||
Or with a direct provider (e.g., `pnpm add @ai-sdk/openai`):
|
||||
|
||||
```ts [server/api/chat.post.ts]
|
||||
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { messages } = await readBody(event)
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-5-nano'),
|
||||
instructions: 'You are a helpful assistant.',
|
||||
messages: await convertToModelMessages(messages)
|
||||
})
|
||||
|
||||
const stream = toUIMessageStream({ stream: result.stream })
|
||||
return createUIMessageStreamResponse({ stream })
|
||||
})
|
||||
```
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UDashboardPanel
|
||||
├── #header → UDashboardNavbar
|
||||
├── #body → UContainer → UChatMessages
|
||||
│ ├── #content → UChatReasoning, UChatTool, Comark
|
||||
│ └── #indicator (loading)
|
||||
└── #footer → UContainer → UChatPrompt
|
||||
└── UChatPromptSubmit
|
||||
```
|
||||
|
||||
## Full page chat
|
||||
|
||||
```vue [pages/chat/[id].vue]
|
||||
<script setup lang="ts">
|
||||
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
|
||||
import { useChat } from '@ai-sdk/vue'
|
||||
import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
|
||||
import highlight from '@comark/nuxt/plugins/highlight'
|
||||
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
|
||||
const input = ref('')
|
||||
|
||||
const { messages, status, error, sendMessage, stop, regenerate } = useChat({
|
||||
onError(error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
function onSubmit() {
|
||||
if (!input.value.trim()) return
|
||||
sendMessage({ text: input.value })
|
||||
input.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Chat" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UContainer>
|
||||
<UChatMessages :messages="messages" :status="status">
|
||||
<template #content="{ message }">
|
||||
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
|
||||
<UChatReasoning
|
||||
v-if="isReasoningUIPart(part)"
|
||||
:text="part.text"
|
||||
:streaming="isPartStreaming(part)"
|
||||
>
|
||||
<Comark
|
||||
:markdown="part.text"
|
||||
:streaming="isPartStreaming(part)"
|
||||
:plugins="[highlight()]"
|
||||
class="*:first:mt-0 *:last:mb-0"
|
||||
/>
|
||||
</UChatReasoning>
|
||||
|
||||
<UChatTool
|
||||
v-else-if="isToolUIPart(part)"
|
||||
:text="getToolName(part)"
|
||||
:streaming="isToolStreaming(part)"
|
||||
/>
|
||||
|
||||
<template v-else-if="isTextUIPart(part)">
|
||||
<Comark
|
||||
v-if="message.role === 'assistant'"
|
||||
:markdown="part.text"
|
||||
:streaming="isPartStreaming(part)"
|
||||
:plugins="[highlight()]"
|
||||
class="*:first:mt-0 *:last:mb-0"
|
||||
/>
|
||||
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
|
||||
{{ part.text }}
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</UChatMessages>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UContainer class="pb-4 sm:pb-6">
|
||||
<UChatPrompt v-model="input" :error="error" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="status" @stop="stop()" @reload="regenerate()" />
|
||||
</UChatPrompt>
|
||||
</UContainer>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Key components
|
||||
|
||||
- `UChatMessages` — scrollable message list with auto-scroll. Props: `messages`, `status`. Slots: `#content` (per message), `#actions`, `#indicator`.
|
||||
- `UChatMessage` — individual bubble. Props: `message`, `side` (`'left'`/`'right'`).
|
||||
- `UChatReasoning` — collapsible reasoning block. Auto-opens during streaming, auto-closes when done. Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai`.
|
||||
- `UChatTool` — tool invocation status. Use `isToolStreaming(part)`. Variants: `'inline'` (default), `'card'`.
|
||||
- `UChatPrompt` — enhanced textarea for chat. Its typed API exposes a selected subset of `UTextarea` props, forwards additional attributes to the underlying textarea, and adds chat-specific props such as `error` and `submitOnEnter`.
|
||||
- `UChatPromptSubmit` — submit button with automatic status handling (send/stop/reload).
|
||||
- `UChatPalette` — layout wrapper for chat inside overlays.
|
||||
|
||||
## Chat in a modal
|
||||
|
||||
```vue
|
||||
<UModal v-model:open="isOpen">
|
||||
<template #content>
|
||||
<UChatPalette>
|
||||
<UChatMessages :messages="messages" :status="status" />
|
||||
|
||||
<template #prompt>
|
||||
<UChatPrompt v-model="input" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="status" />
|
||||
</UChatPrompt>
|
||||
</template>
|
||||
</UChatPalette>
|
||||
</template>
|
||||
</UModal>
|
||||
```
|
||||
|
||||
## With model selector
|
||||
|
||||
```vue
|
||||
<UChatPrompt v-model="input" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="status" />
|
||||
|
||||
<template #footer>
|
||||
<USelect
|
||||
v-model="model"
|
||||
:icon="models.find(m => m.value === model)?.icon"
|
||||
placeholder="Select a model"
|
||||
variant="ghost"
|
||||
:items="models"
|
||||
/>
|
||||
</template>
|
||||
</UChatPrompt>
|
||||
```
|
||||
|
||||
## Conversation sidebar
|
||||
|
||||
Combine with dashboard layout for a ChatGPT-like interface:
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header>
|
||||
<UButton icon="i-lucide-plus" label="New chat" block />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<UNavigationMenu :items="conversations" orientation="vertical" />
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,233 @@
|
||||
# Dashboard Layout
|
||||
|
||||
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
|
||||
|
||||
## When to use
|
||||
|
||||
- Admin panels, back-office UIs
|
||||
- Email clients, project management tools
|
||||
- Any app with a persistent sidebar and content panels
|
||||
- Combine with chat or editor layouts for specialized dashboards
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
└── NuxtLayout (dashboard)
|
||||
└── UDashboardGroup
|
||||
├── UDashboardSidebar
|
||||
│ ├── #header (logo, search button)
|
||||
│ ├── #default (navigation) — receives { collapsed } slot prop
|
||||
│ └── #footer (user menu)
|
||||
└── NuxtPage
|
||||
└── UDashboardPanel
|
||||
├── #header → UDashboardNavbar + UDashboardToolbar
|
||||
├── #body (scrollable content)
|
||||
└── #footer (optional)
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Home',
|
||||
icon: 'i-lucide-house',
|
||||
to: '/dashboard'
|
||||
}, {
|
||||
label: 'Inbox',
|
||||
icon: 'i-lucide-inbox',
|
||||
to: '/dashboard/inbox'
|
||||
}, {
|
||||
label: 'Users',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/dashboard/users'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-settings',
|
||||
to: '/dashboard/settings'
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header="{ collapsed }">
|
||||
<UDashboardSearchButton :collapsed="collapsed" />
|
||||
</template>
|
||||
|
||||
<template #default="{ collapsed }">
|
||||
<UNavigationMenu
|
||||
:collapsed="collapsed"
|
||||
:items="items"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer="{ collapsed }">
|
||||
<UButton
|
||||
:icon="collapsed ? 'i-lucide-log-out' : undefined"
|
||||
:label="collapsed ? undefined : 'Sign out'"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
block
|
||||
/>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
```vue [pages/dashboard/index.vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton icon="i-lucide-plus" label="New" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<!-- Page content -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- Forgetting `definePageMeta({ layout: 'dashboard' })` — the page won't use the dashboard layout without it.
|
||||
- Putting content directly in `UDashboardPanel` without using `#body` slot — content won't scroll properly.
|
||||
- Not handling the `collapsed` slot prop — sidebar content should adapt when collapsed (hide labels, center icons).
|
||||
|
||||
## Key components
|
||||
|
||||
### DashboardGroup
|
||||
|
||||
Root wrapper. Manages sidebar state and persistence.
|
||||
|
||||
| Prop | Default | Purpose |
|
||||
| ------------- | ------------- | ------------------------------------- |
|
||||
| `storage` | `'cookie'` | `'cookie'`, `'localStorage'`, `false` |
|
||||
| `storage-key` | `'dashboard'` | Storage key name |
|
||||
|
||||
### DashboardSidebar
|
||||
|
||||
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
|
||||
|
||||
| Prop | Default | Purpose |
|
||||
| ------------- | ------------- | -------------------------------------------- |
|
||||
| `resizable` | `false` | Drag to resize |
|
||||
| `collapsible` | `false` | Collapse when dragged to edge |
|
||||
| `side` | `'left'` | `'left'` or `'right'` |
|
||||
| `mode` | `'slideover'` | Mobile: `'modal'`, `'slideover'`, `'drawer'` |
|
||||
|
||||
All slots receive `{ collapsed, collapse }` — `collapsed` is the boolean state, `collapse(value)` toggles it programmatically. Use `v-model:collapsed` and `v-model:open` (mobile) for state control.
|
||||
|
||||
### DashboardPanel
|
||||
|
||||
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw, no scroll) slots.
|
||||
|
||||
### DashboardNavbar / DashboardToolbar
|
||||
|
||||
Navbar: `#leading`, `#left`, `#default`, `#right` slots + `title` prop. Use `UDashboardSidebarCollapse` in `#leading` to toggle sidebar on mobile.
|
||||
Toolbar: same slots, sits below navbar for filters/actions.
|
||||
|
||||
### UNavigationMenu in sidebar
|
||||
|
||||
Always pass `:collapsed="collapsed"` to `UNavigationMenu` inside a collapsible sidebar — it auto-hides labels and centers icons. Use `NavigationMenuItem[][]` (array of arrays) for separate groups (main nav + footer links).
|
||||
|
||||
## Multi-panel (list-detail)
|
||||
|
||||
```vue [pages/dashboard/inbox.vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel id="inbox-list" resizable>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Inbox" />
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Email list -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
|
||||
<UDashboardPanel id="inbox-detail" class="hidden lg:flex">
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Message" />
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Email content -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With toolbar
|
||||
|
||||
```vue
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Users" />
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UInput icon="i-lucide-search" placeholder="Search..." />
|
||||
</template>
|
||||
<template #right>
|
||||
<USelect :items="['All', 'Active', 'Inactive']" />
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
```
|
||||
|
||||
## With search
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar>
|
||||
<template #header>
|
||||
<UDashboardSearchButton />
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
|
||||
<UDashboardSearch :groups="searchGroups" />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Right sidebar
|
||||
|
||||
```vue
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<!-- Left sidebar -->
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
|
||||
<UDashboardSidebar side="right" resizable>
|
||||
<!-- Right sidebar -->
|
||||
</UDashboardSidebar>
|
||||
</UDashboardGroup>
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
# Docs Layout
|
||||
|
||||
Build documentation sites with sidebar navigation, table of contents, and surround links.
|
||||
|
||||
## When to use
|
||||
|
||||
- Technical documentation sites
|
||||
- Knowledge bases, help centers
|
||||
- Any content-heavy site with hierarchical navigation
|
||||
|
||||
> Requires `@nuxt/content` — see [conventions](../guidelines/conventions.md#content-module-integration) for setup (module order + `@source`).
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
├── UHeader
|
||||
├── UMain
|
||||
│ └── NuxtLayout (docs)
|
||||
│ └── UPage
|
||||
│ ├── #left → UPageAside → UContentNavigation
|
||||
│ └── NuxtPage
|
||||
│ ├── UPageHeader
|
||||
│ ├── UPageBody → ContentRenderer + UContentSurround
|
||||
│ └── #right → UContentToc
|
||||
└── UFooter
|
||||
```
|
||||
|
||||
## App shell
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
||||
|
||||
provide('navigation', navigation)
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Docs',
|
||||
to: '/docs/getting-started',
|
||||
active: route.path.startsWith('/docs')
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UContentSearchButton />
|
||||
<UColorModeButton />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UMain>
|
||||
|
||||
<UFooter />
|
||||
|
||||
<UContentSearch :navigation="navigation" />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```vue [layouts/docs.vue]
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UPageAside>
|
||||
<UContentNavigation :navigation="navigation" />
|
||||
</UPageAside>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
```vue [pages/docs/[...slug].vue]
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
definePageMeta({ layout: 'docs' })
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => {
|
||||
return queryCollection('docs').path(route.path).first()
|
||||
})
|
||||
|
||||
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
|
||||
return queryCollectionItemSurroundings('docs', route.path)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader :title="page.title" :description="page.description" />
|
||||
|
||||
<UPageBody>
|
||||
<ContentRenderer :value="page" />
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UContentSurround :surround="surround" />
|
||||
</UPageBody>
|
||||
|
||||
<template #right>
|
||||
<UContentToc :links="page.body.toc.links" />
|
||||
</template>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
### How nesting works
|
||||
|
||||
The outer `UPage` in the layout handles the **left sidebar**. The inner `UPage` in the page handles the **right sidebar**. They nest correctly — this is intentional.
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- Not providing navigation via `provide`/`inject` — the layout needs it from the app shell.
|
||||
- Forgetting `UContentSearch` in app.vue — search won't work without it.
|
||||
- Using `UContentSearchButton` without `UContentSearch` — the button opens search, but the search component must exist.
|
||||
|
||||
## Key components
|
||||
|
||||
- `UPage` — multi-column grid with `#left`, `#default`, `#right` slots
|
||||
- `UPageAside` — sticky sidebar wrapper (visible from `lg` breakpoint)
|
||||
- `UContentNavigation` — sidebar navigation tree from Nuxt Content
|
||||
- `UContentToc` — table of contents from page headings
|
||||
- `UContentSurround` — prev/next links
|
||||
- `UContentSearch` / `UContentSearchButton` — search command palette
|
||||
- `UPageAnchors` — simpler alternative to full TOC
|
||||
@@ -0,0 +1,153 @@
|
||||
# Editor Layout
|
||||
|
||||
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
|
||||
|
||||
## When to use
|
||||
|
||||
- Note-taking apps, CMS editors
|
||||
- Collaborative editing interfaces
|
||||
- Any rich text editing need (supports JSON, HTML, and Markdown)
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UEditor
|
||||
├── UEditorToolbar (fixed / bubble / floating)
|
||||
├── UEditorDragHandle
|
||||
├── UEditorSuggestionMenu
|
||||
├── UEditorMentionMenu
|
||||
└── UEditorEmojiMenu
|
||||
```
|
||||
|
||||
## Basic editor
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const content = ref({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: 'text', text: 'Hello World' }]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Start writing...' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UEditor v-slot="{ editor }" v-model="content">
|
||||
<UEditorToolbar :editor="editor" />
|
||||
<UEditorSuggestionMenu :editor="editor" />
|
||||
<UEditorMentionMenu
|
||||
:editor="editor"
|
||||
:items="[
|
||||
{ label: 'Benjamin', avatar: { src: 'https://github.com/benjamincanac.png' } },
|
||||
{ label: 'Sébastien', avatar: { src: 'https://github.com/atinux.png' } }
|
||||
]"
|
||||
/>
|
||||
<UEditorEmojiMenu :editor="editor" />
|
||||
<UEditorDragHandle :editor="editor" />
|
||||
</UEditor>
|
||||
</template>
|
||||
```
|
||||
|
||||
> If you encounter prosemirror-related errors, add prosemirror packages to `vite.optimizeDeps.include` in `nuxt.config.ts`.
|
||||
|
||||
## Key components
|
||||
|
||||
- `UEditor` — rich text editor. `v-model` accepts JSON (default), HTML, or Markdown via `content-type` prop. Default slot provides `{ editor, handlers }` — `editor` is the Tiptap instance, `handlers` contains action functions for toolbar/menus.
|
||||
- `UEditorToolbar` — toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines).
|
||||
- `UEditorDragHandle` — block drag-and-drop handle.
|
||||
- `UEditorSuggestionMenu` — slash command menu (type `/` to open).
|
||||
- `UEditorMentionMenu` — `@` mention menu.
|
||||
- `UEditorEmojiMenu` — emoji picker (type `:` to open).
|
||||
|
||||
## Toolbar modes
|
||||
|
||||
```vue
|
||||
<!-- Fixed (default) — always visible at top -->
|
||||
<UEditorToolbar :editor="editor" />
|
||||
|
||||
<!-- Bubble — appears on text selection -->
|
||||
<UEditorToolbar :editor="editor" layout="bubble" />
|
||||
|
||||
<!-- Floating — appears on empty lines -->
|
||||
<UEditorToolbar :editor="editor" layout="floating" />
|
||||
```
|
||||
|
||||
## Content types
|
||||
|
||||
```vue
|
||||
<!-- JSON (default) -->
|
||||
<UEditor v-model="jsonContent" />
|
||||
|
||||
<!-- HTML -->
|
||||
<UEditor v-model="htmlContent" content-type="html" />
|
||||
|
||||
<!-- Markdown -->
|
||||
<UEditor v-model="markdownContent" content-type="markdown" />
|
||||
```
|
||||
|
||||
## With document sidebar
|
||||
|
||||
Combine with Dashboard layout for a multi-document editor:
|
||||
|
||||
```vue [layouts/editor.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header>
|
||||
<UButton icon="i-lucide-plus" label="New document" block />
|
||||
</template>
|
||||
|
||||
<template #default="{ collapsed }">
|
||||
<UNavigationMenu
|
||||
:collapsed="collapsed"
|
||||
:items="documents.map(doc => ({
|
||||
label: doc.title,
|
||||
to: `/editor/${doc.id}`,
|
||||
icon: 'i-lucide-file-text'
|
||||
}))"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
```vue [pages/editor/[id].vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'editor' })
|
||||
|
||||
const content = ref({ type: 'doc', content: [] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Editor">
|
||||
<template #right>
|
||||
<UButton label="Save" icon="i-lucide-save" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<UContainer class="py-8">
|
||||
<UEditor v-slot="{ editor }" v-model="content">
|
||||
<UEditorToolbar :editor="editor" />
|
||||
<UEditorSuggestionMenu :editor="editor" />
|
||||
<UEditorEmojiMenu :editor="editor" />
|
||||
<UEditorDragHandle :editor="editor" />
|
||||
</UEditor>
|
||||
</UContainer>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,185 @@
|
||||
# Landing Page Layout
|
||||
|
||||
Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components.
|
||||
|
||||
## When to use
|
||||
|
||||
- Marketing sites, product pages, company sites
|
||||
- Blog and content pages
|
||||
- Pricing, changelog, portfolio pages
|
||||
- Any public-facing page that isn't a dashboard or documentation
|
||||
|
||||
## App shell
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Features',
|
||||
to: '#features'
|
||||
}, {
|
||||
label: 'Pricing',
|
||||
to: '/pricing'
|
||||
}, {
|
||||
label: 'Blog',
|
||||
to: '/blog'
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UButton label="Sign in" color="neutral" variant="ghost" />
|
||||
<UButton label="Get started" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p class="text-muted text-sm">Copyright © {{ new Date().getFullYear() }}</p>
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton icon="i-simple-icons-github" color="neutral" variant="ghost" to="https://github.com" target="_blank" />
|
||||
</template>
|
||||
</UFooter>
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
- Forgetting the `#body` slot on `UHeader` — this is the mobile menu content. Without it, mobile users have no navigation.
|
||||
- Using `variant="solid"` for both header and hero buttons — the header button should be lower weight than the hero CTA.
|
||||
|
||||
## Landing page
|
||||
|
||||
```vue [pages/index.vue]
|
||||
<template>
|
||||
<UPageHero
|
||||
title="Build faster with Nuxt UI"
|
||||
description="A comprehensive Vue UI component library."
|
||||
:links="[
|
||||
{ label: 'Get started', to: '/docs', icon: 'i-lucide-square-play' },
|
||||
{ label: 'Learn more', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' }
|
||||
]"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<img src="/hero-image.png" alt="App screenshot" class="rounded-lg shadow-2xl ring ring-default" />
|
||||
</UPageHero>
|
||||
|
||||
<UPageSection
|
||||
id="features"
|
||||
headline="Features"
|
||||
title="Everything you need"
|
||||
description="A comprehensive suite of components and utilities."
|
||||
:features="[
|
||||
{ title: 'Accessible', description: 'Built on Reka UI with full ARIA support.', icon: 'i-lucide-accessibility' },
|
||||
{ title: 'Customizable', description: 'Tailwind Variants theming with full control.', icon: 'i-lucide-palette' },
|
||||
{ title: 'Responsive', description: 'Mobile-first components.', icon: 'i-lucide-monitor-smartphone' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<UPageCTA
|
||||
title="Trusted by thousands of developers"
|
||||
description="Join the community and start building today."
|
||||
:links="[
|
||||
{ label: 'Get started', color: 'neutral' },
|
||||
{ label: 'Star on GitHub', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<UPageSection id="pricing" headline="Pricing" title="Simple, transparent pricing">
|
||||
<UPricingPlans
|
||||
:plans="[
|
||||
{ title: 'Free', price: '$0', description: 'For personal projects', features: ['10 components', 'Community support'] },
|
||||
{ title: 'Pro', price: '$99', description: 'For teams', features: ['All components', 'Priority support'], highlight: true },
|
||||
{ title: 'Enterprise', price: 'Custom', description: 'For large teams', features: ['Custom components', 'Dedicated support'] }
|
||||
]"
|
||||
/>
|
||||
</UPageSection>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Key components
|
||||
|
||||
- `UPageHero` — hero with title, description, links, and optional media. Use `orientation="horizontal"` for side-by-side layout.
|
||||
- `UPageSection` — content section with headline, title, description, and `features` grid. Use `id` for anchor links.
|
||||
- `UPageCTA` — call to action block.
|
||||
- `UPageGrid` / `UPageCard` — card grid for features, testimonials, etc.
|
||||
- `UPageFeature` — individual feature item.
|
||||
- `UPageLogos` — logo wall for social proof.
|
||||
- `UPricingPlans` / `UPricingTable` — pricing cards and comparison tables.
|
||||
- `UFooterColumns` — multi-column footer with link groups (used inside `UFooter`).
|
||||
|
||||
## Variations
|
||||
|
||||
### Alternating feature sections
|
||||
|
||||
```vue
|
||||
<UPageSection title="Feature A" orientation="horizontal">
|
||||
<img src="/feature-a.png" />
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection title="Feature B" orientation="horizontal" reverse>
|
||||
<img src="/feature-b.png" />
|
||||
</UPageSection>
|
||||
```
|
||||
|
||||
### Blog listing
|
||||
|
||||
```vue [pages/blog/index.vue]
|
||||
<script setup lang="ts">
|
||||
const { data: posts } = await useAsyncData('posts', () => queryCollection('posts').all())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHero title="Blog" description="The latest news and updates." />
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UBlogPosts>
|
||||
<UBlogPost v-for="post in posts" :key="post.path" v-bind="post" :to="post.path" />
|
||||
</UBlogPosts>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Changelog
|
||||
|
||||
```vue [pages/changelog.vue]
|
||||
<script setup lang="ts">
|
||||
const { data: versions } = await useAsyncData('versions', () => queryCollection('changelog').all())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHero title="Changelog" />
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UChangelogVersions>
|
||||
<UChangelogVersion v-for="version in versions" :key="version.path" v-bind="version" />
|
||||
</UChangelogVersions>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
# Auth Forms
|
||||
|
||||
## UAuthForm (recommended)
|
||||
|
||||
`UAuthForm` provides a complete auth form with fields, providers, validation, and submit — no manual `UForm` + `UFormField` wiring needed. Wrap it in `UPageCard` for a polished look.
|
||||
|
||||
```vue [pages/login.vue]
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'
|
||||
|
||||
const fields: AuthFormField[] = [{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true
|
||||
}, {
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
placeholder: 'Enter your password',
|
||||
required: true
|
||||
}, {
|
||||
name: 'remember',
|
||||
label: 'Remember me',
|
||||
type: 'checkbox'
|
||||
}]
|
||||
|
||||
const providers = [{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => navigateTo('/auth/google', { external: true })
|
||||
}, {
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => navigateTo('/auth/github', { external: true })
|
||||
}]
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string('Password is required').min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
// payload.data contains validated fields
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-dvh items-center justify-center">
|
||||
<UPageCard class="w-full max-w-md">
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:providers="providers"
|
||||
title="Welcome back!"
|
||||
description="Sign in to your account."
|
||||
icon="i-lucide-lock"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template #password-hint>
|
||||
<ULink to="/forgot-password" class="text-primary font-medium">Forgot password?</ULink>
|
||||
</template>
|
||||
<template #footer>
|
||||
Don't have an account? <ULink to="/signup" class="text-primary font-medium">Sign up</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### UAuthForm key props
|
||||
|
||||
| Prop | Purpose |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `title`, `description`, `icon` | Header content |
|
||||
| `fields` | `AuthFormField[]` — each has `name`, `type`, `label`, `placeholder`, `required` |
|
||||
| `providers` | `ButtonProps[]` — social login buttons shown above/below the form |
|
||||
| `schema` | Zod/Valibot schema for validation |
|
||||
| `submit` | Customize submit button: `{ label: 'Sign in', block: true }` |
|
||||
| `separator` | Text between providers and fields (default: `'or'`) |
|
||||
|
||||
### UAuthForm key slots
|
||||
|
||||
| Slot | Purpose |
|
||||
| ---------------- | --------------------------------------------- |
|
||||
| `#description` | Override description (e.g., add sign-up link) |
|
||||
| `#password-hint` | "Forgot password?" link on password field |
|
||||
| `#validation` | Custom error display (e.g., `UAlert`) |
|
||||
| `#footer` | Terms of service, sign-up link |
|
||||
| `#<field>-field` | Override a specific field's rendering |
|
||||
|
||||
## Custom auth layout
|
||||
|
||||
For layouts where `UAuthForm` is too opinionated, use `UCard` + `UForm` + `UFormField` directly.
|
||||
|
||||
```vue [pages/login.vue]
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string().min(1, 'Password is required')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({ email: '', password: '' })
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
// event.data contains validated fields
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-dvh items-center justify-center">
|
||||
<UCard class="w-full max-w-sm">
|
||||
<template #header>
|
||||
<div class="text-center">
|
||||
<h1 class="text-xl font-semibold text-default">Welcome back</h1>
|
||||
<p class="mt-1 text-sm text-muted">Sign in to your account</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormField name="email" label="Email">
|
||||
<UInput v-model="state.email" type="email" placeholder="you@example.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="password" label="Password">
|
||||
<template #hint>
|
||||
<NuxtLink to="/forgot-password" class="text-sm text-primary">Forgot password?</NuxtLink>
|
||||
</template>
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" label="Sign in" block />
|
||||
</UForm>
|
||||
|
||||
<template #footer>
|
||||
<p class="text-center text-sm text-muted">
|
||||
Don't have an account?
|
||||
<NuxtLink to="/signup" class="text-primary font-medium">Sign up</NuxtLink>
|
||||
</p>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Prefer `UAuthForm` with `UPageCard` for standard auth pages — handles layout, providers, validation, and submit
|
||||
- Use `import * as z from 'zod'` and `z.email()` (Zod 4 syntax)
|
||||
- Type the submit handler: `function onSubmit(event: FormSubmitEvent<Schema>)` — access validated data via `event.data`
|
||||
- Center auth forms with `flex min-h-dvh items-center justify-center`
|
||||
- Place "Forgot password?" link as `#password-hint` slot on `UAuthForm`, or `#hint` slot on `UFormField`
|
||||
- Social login buttons: use `providers` prop on `UAuthForm`, or add manually with `<USeparator label="or" />`
|
||||
@@ -0,0 +1,225 @@
|
||||
# Data Tables
|
||||
|
||||
Complete patterns for displaying and managing tabular data.
|
||||
|
||||
## Basic table
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
const data = ref([
|
||||
{ name: 'Alice', email: 'alice@example.com', role: 'Admin' },
|
||||
{ name: 'Bob', email: 'bob@example.com', role: 'Editor' }
|
||||
])
|
||||
|
||||
const columns: TableColumn<typeof data.value[number]>[] = [{
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'role',
|
||||
header: 'Role'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data" :columns="columns" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## With search and filters (dashboard)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
const search = ref('')
|
||||
const roleFilter = ref('All')
|
||||
|
||||
const rows = ref([
|
||||
{ name: 'Alice', email: 'alice@example.com', role: 'Admin', status: 'Active' },
|
||||
{ name: 'Bob', email: 'bob@example.com', role: 'Editor', status: 'Inactive' }
|
||||
])
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'email', header: 'Email' },
|
||||
{ accessorKey: 'role', header: 'Role' },
|
||||
{ accessorKey: 'status', header: 'Status' },
|
||||
{ id: 'actions' }
|
||||
]
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter(row => {
|
||||
const matchesSearch = !search.value || row.name.toLowerCase().includes(search.value.toLowerCase())
|
||||
const matchesRole = roleFilter.value === 'All' || row.role === roleFilter.value
|
||||
return matchesSearch && matchesRole
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Users" />
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UInput v-model="search" icon="i-lucide-search" placeholder="Search users..." />
|
||||
</template>
|
||||
<template #right>
|
||||
<USelect v-model="roleFilter" :items="['All', 'Admin', 'Editor', 'Viewer']" />
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UTable :data="filteredRows" :columns="columns">
|
||||
<template #status-cell="{ row }">
|
||||
<UBadge :color="row.original.status === 'Active' ? 'success' : 'neutral'" :label="row.original.status" variant="subtle" />
|
||||
</template>
|
||||
|
||||
<template #actions-cell="{ row }">
|
||||
<UDropdownMenu
|
||||
:items="[
|
||||
[{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit(row.original) }],
|
||||
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: () => remove(row.original) }]
|
||||
]"
|
||||
>
|
||||
<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With row selection
|
||||
|
||||
Row selection uses TanStack Table's `rowSelection` state — a `Record<string, boolean>` keyed by row index.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const table = useTemplateRef('table')
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />
|
||||
|
||||
<div class="px-4 py-3.5 text-sm text-muted">
|
||||
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
|
||||
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Add a checkbox column using the `h` function. Use tri-state `modelValue` (`true`, `false`, or `'indeterminate'`) for the "select all" header:
|
||||
|
||||
```ts
|
||||
import { h } from 'vue'
|
||||
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
|
||||
const columns: TableColumn[] = [{
|
||||
id: 'select',
|
||||
header: ({ table }) => h(UCheckbox, {
|
||||
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||
'aria-label': 'Select all'
|
||||
}),
|
||||
cell: ({ row }) => h(UCheckbox, {
|
||||
'modelValue': row.getIsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||
'aria-label': 'Select row'
|
||||
})
|
||||
},
|
||||
// ... other columns
|
||||
]
|
||||
```
|
||||
|
||||
## With pagination
|
||||
|
||||
Use `v-model:pagination` on `UTable` with TanStack's `getPaginationRowModel`, then wire `UPagination` to the table API. `UPagination`'s `total` is total **items** (not pages) — it calculates page count from `total / items-per-page`.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { getPaginationRowModel } from '@tanstack/vue-table'
|
||||
|
||||
const table = useTemplateRef('table')
|
||||
|
||||
const pagination = ref({
|
||||
pageIndex: 0,
|
||||
pageSize: 5
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable
|
||||
ref="table"
|
||||
v-model:pagination="pagination"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end p-4">
|
||||
<UPagination
|
||||
:page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
|
||||
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
|
||||
:total="table?.tableApi?.getFilteredRowModel().rows.length"
|
||||
@update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With async data (Nuxt)
|
||||
|
||||
Use `status === 'pending' || status === 'idle'` for loading state — `idle` covers the initial render before `useLazyFetch` starts.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, status } = useLazyFetch('/api/users', { server: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data" :columns="columns" :loading="status === 'pending' || status === 'idle'" />
|
||||
</template>
|
||||
```
|
||||
|
||||
For server-side pagination:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const page = ref(1)
|
||||
|
||||
const { data, status } = await useAsyncData(
|
||||
'users',
|
||||
() => $fetch('/api/users', { query: { page: page.value } }),
|
||||
{ watch: [page] }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data?.items" :columns="columns" :loading="status === 'pending'" />
|
||||
|
||||
<div class="flex justify-end p-4">
|
||||
<UPagination v-model="page" :total="data?.total" :items-per-page="data?.pageSize" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Table is built on [TanStack Table v8](https://tanstack.com/table/v8) — columns use `ColumnDef` format with `accessorKey`, `header`, `cell`
|
||||
- Use `#<column>-cell` and `#<column>-header` template slots to customize rendering with Vue templates
|
||||
- Alternatively, use the `h` function inside `header` and `cell` column properties for inline rendering
|
||||
- Row data in slots is accessed via `row.original` (not `row` directly)
|
||||
- Use `v-model:row-selection` for selection, `v-model:sorting` for sort state
|
||||
- Wrap tables in `UDashboardPanel` with `#header` toolbar for the dashboard pattern
|
||||
- For empty states, use the `#empty` slot
|
||||
@@ -0,0 +1,90 @@
|
||||
# Navigation
|
||||
|
||||
Patterns for headers, sidebars, breadcrumbs, and tab navigation.
|
||||
|
||||
## Header with mobile menu
|
||||
|
||||
`UHeader` default slot is desktop nav, `#body` is the mobile menu. Without `#body`, mobile users have no navigation.
|
||||
|
||||
```vue
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UButton label="Sign in" color="neutral" variant="ghost" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
|
||||
</template>
|
||||
</UHeader>
|
||||
```
|
||||
|
||||
> Full app shell example in [landing layout](../layouts/landing.md).
|
||||
|
||||
## Sidebar navigation (dashboard)
|
||||
|
||||
See [dashboard layout](../layouts/dashboard.md) for the full sidebar pattern with `UDashboardSidebar` + `UNavigationMenu`. Key points:
|
||||
|
||||
- Pass `:collapsed="collapsed"` to `UNavigationMenu` inside collapsible sidebars
|
||||
- Use `NavigationMenuItem[][]` (nested arrays) for separate nav groups
|
||||
- Use `#footer` slot for user menu with `UDropdownMenu`
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const segments = route.path.split('/').filter(Boolean)
|
||||
return segments.map((segment, index) => ({
|
||||
label: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||
to: '/' + segments.slice(0, index + 1).join('/')
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UBreadcrumb :items="breadcrumbs" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tab navigation (within a page)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
label: 'Overview',
|
||||
icon: 'i-lucide-layout-dashboard',
|
||||
slot: 'overview' as const
|
||||
}, {
|
||||
label: 'Activity',
|
||||
icon: 'i-lucide-activity',
|
||||
slot: 'activity' as const
|
||||
}, {
|
||||
label: 'Members',
|
||||
icon: 'i-lucide-users',
|
||||
slot: 'members' as const
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items">
|
||||
<template #overview>
|
||||
<!-- Overview content -->
|
||||
</template>
|
||||
<template #activity>
|
||||
<!-- Activity feed -->
|
||||
</template>
|
||||
<template #members>
|
||||
<!-- Members list -->
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,172 @@
|
||||
# Overlays
|
||||
|
||||
Patterns for modals, slideovers, drawers, and command palettes.
|
||||
|
||||
## Confirmation dialog
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
|
||||
function confirmDelete() {
|
||||
// perform delete
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Delete" color="error" variant="soft" @click="isOpen = true" />
|
||||
|
||||
<UModal v-model:open="isOpen" title="Delete item" description="This action cannot be undone. Are you sure?" :ui="{ footer: 'justify-end' }">
|
||||
<template #footer="{ close }">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
|
||||
<UButton label="Delete" color="error" @click="confirmDelete" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Programmatic confirmation (useOverlay)
|
||||
|
||||
Reusable pattern — no template state needed at the call site.
|
||||
|
||||
```vue [components/ConfirmModal.vue]
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [confirmed: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :close="{ onClick: () => emit('close', false) }" :title="title" :description="description">
|
||||
<template #footer>
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="emit('close', false)" />
|
||||
<UButton label="Confirm" color="error" @click="emit('close', true)" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts
|
||||
// Usage anywhere
|
||||
const overlay = useOverlay()
|
||||
const confirm = overlay.create(ConfirmModal)
|
||||
|
||||
async function deleteItem(item) {
|
||||
const instance = confirm.open({
|
||||
title: 'Delete item',
|
||||
description: `Are you sure you want to delete "${item.name}"?`
|
||||
})
|
||||
|
||||
if (await instance.result) {
|
||||
// user confirmed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Form in a slideover
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.email()
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
|
||||
function onSave() {
|
||||
// save user
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Add user" @click="isOpen = true" />
|
||||
|
||||
<USlideover v-model:open="isOpen" title="Add user" description="Fill in the details below.">
|
||||
<template #body>
|
||||
<UForm id="user-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave">
|
||||
<UFormField name="name" label="Name">
|
||||
<UInput v-model="state.name" />
|
||||
</UFormField>
|
||||
<UFormField name="email" label="Email">
|
||||
<UInput v-model="state.email" type="email" />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
|
||||
<UButton type="submit" form="user-form" label="Save" />
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Command palette
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
|
||||
defineShortcuts({
|
||||
meta_k: () => { isOpen.value = true }
|
||||
})
|
||||
|
||||
const groups = [{
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
items: [
|
||||
{ label: 'New file', icon: 'i-lucide-file-plus', kbds: ['meta', 'n'], onSelect: () => newFile() },
|
||||
{ label: 'New folder', icon: 'i-lucide-folder-plus', onSelect: () => newFolder() }
|
||||
]
|
||||
}, {
|
||||
id: 'navigation',
|
||||
label: 'Navigation',
|
||||
items: [
|
||||
{ label: 'Dashboard', icon: 'i-lucide-house', to: '/dashboard' },
|
||||
{ label: 'Settings', icon: 'i-lucide-settings', to: '/settings' }
|
||||
]
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Search..." icon="i-lucide-search" color="neutral" variant="outline" @click="isOpen = true" />
|
||||
|
||||
<UCommandPalette v-model:open="isOpen" :groups="groups" placeholder="Type a command or search..." />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Drawer (bottom sheet)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Options" @click="isOpen = true" />
|
||||
|
||||
<UDrawer v-model:open="isOpen" title="Options">
|
||||
<template #body>
|
||||
<div class="space-y-2 p-4">
|
||||
<UButton label="Share" icon="i-lucide-share" block variant="ghost" />
|
||||
<UButton label="Export" icon="i-lucide-download" block variant="ghost" />
|
||||
<USeparator />
|
||||
<UButton label="Delete" icon="i-lucide-trash" block variant="ghost" color="error" />
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</template>
|
||||
```
|
||||
Reference in New Issue
Block a user