# Installation

\> \[!NOTE]
\> See: /docs/getting-started/installation/vue
\> Looking for the Vue version?

## Setup

### Add to a Nuxt project

Install the Nuxt UI package
\`\`\`bash
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun add @nuxt/ui tailwindcss
\`\`\`
Add the Nuxt UI module in your nuxt.config.ts
\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui']
})
\`\`\`
Import Tailwind CSS and Nuxt UI in your CSS
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui'],
css: \['\~/assets/css/main.css']
})
\`\`\`
\> \[!TIP]
\> See: https\://nuxt.com/docs/getting-started/layers
\> When using \[Nuxt Layers]\(https\://nuxt.com/docs/getting-started/layers), the module automatically generates \[\`@source\`]\(https\://tailwindcss.com/docs/functions-and-directives#source-directive) directives for each layer directory, ensuring Tailwind CSS scans all your layer source files for utility classes.
\> \[!NOTE]
\> It's recommended to install the \[Tailwind CSS IntelliSense]\(https\://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
\> \`\`\`json
\> {
\> "files.associations": {
\> "\*.css": "tailwindcss"
\> },
\> "editor.quickSuggestions": {
\> "strings": "on"
\> },
\> "tailwindCSS.classAttributes": \["class", "ui"],
\> "tailwindCSS.experimental.classRegex": \[
\> \["\['\\"\`]\(\[^'\\"\`]\*)\['\\"\`]"]
\> ]
\> }
\>
\> \`\`\`
Wrap your app with App component
\`\`\`vue
\<template>
\<UApp>
\<NuxtPage />
\</UApp>
\</template>
\`\`\`
\> \[!NOTE]
\> See: /docs/components/app
\> The \`App\` component provides global configurations and is required for Toast, Tooltip components to work as well as Programmatic Overlays.

### Use a Nuxt template

Get started with one of our [official templates](https://ui.nuxt.com/templates) by using the `Use this template` button on GitHub or the CLI:

\`\`\`bash
npm create nuxt\@latest -- -t ui
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/landing
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/docs
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/saas
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/dashboard
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/chat
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/portfolio
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/changelog
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/editor
\`\`\`

\*\*Starter\*\*
A minimal template to get started with Nuxt UI.
\*\*Landing\*\*
A modern landing page template powered by Nuxt Content.
\*\*Docs\*\*
A documentation template powered by Nuxt Content.
\*\*SaaS\*\*
A SaaS template with landing, pricing, docs and blog powered by Nuxt Content.
\*\*Dashboard\*\*
A dashboard template with multi-column layout for building sophisticated admin interfaces.
\*\*Chat\*\*
An AI chatbot template to build your own chatbot powered by Vercel AI SDK.
\*\*Portfolio\*\*
A sleek portfolio template to showcase your work, skills and blog powered by Nuxt Content.
\*\*Changelog\*\*
A changelog template to display your repository releases notes from GitHub powered by Nuxt MDC.
\*\*Editor\*\*
A rich text editor template powered by TipTap with support for markdown, HTML, and JSON content types.

## Options

You can customize Nuxt UI by providing options in your `nuxt.config.ts`.

### `prefix`

Use the `prefix` option to change the prefix of the components.

- Default: `U`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    prefix: 'Nuxt'
  }
})
```

### `fonts`

Use the `fonts` option to enable or disable the [`@nuxt/fonts`](https://github.com/nuxt/fonts){rel="&#x22;nofollow&#x22;"} module.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    fonts: false
  }
})
```

### `colorMode`

Use the `colorMode` option to enable or disable the [`@nuxt/color-mode`](https://github.com/nuxt-modules/color-mode){rel="&#x22;nofollow&#x22;"} module.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    colorMode: false
  }
})
```

### `theme.colors`

Use the `theme.colors` option to define the dynamic color aliases used to generate components theme.

- Default: `['primary', 'secondary', 'success', 'info', 'warning', 'error']`{.inline,language-ts-type,shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      colors: ['primary', 'error']
    }
  }
})
```

\> \[!TIP]
\> See: /docs/getting-started/theme/design-system#colors
\> Learn more about color customization and theming in the Theme section.

### `theme.transitions`

Use the `theme.transitions` option to enable or disable transitions on components.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      transitions: false
    }
  }
})
```

\> \[!NOTE]
\> This option adds the \`transition-colors\` class on components with hover or active states.

### `theme.defaultVariants`

Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.

- Default: `{ color: 'primary', size: 'md' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-11}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      defaultVariants: {
        color: 'neutral',
        size: 'sm'
      }
    }
  }
})
```

### `theme.prefix` `4.2+`

Use the `theme.prefix` option to configure the same prefix you set on your Tailwind CSS import. This ensures Nuxt UI components use the correct prefixed utility classes and CSS variables.

\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui'],
css: \['\~/assets/css/main.css'],
ui: {
theme: {
prefix: 'tw'
}
}
})
\`\`\`
\`\`\`css
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";
\`\`\`

\> \[!WARNING]
\> See: https\://fonts.nuxt.com/get-started/configuration#processcssvariables
\> You might need to enable \`fonts.processCSSVariables\` to use the prefix option with the \`@nuxt/fonts\` module:
\> \`\`\`ts
\> export default defineNuxtConfig({
\> modules: \['@nuxt/ui'],
\> css: \['\~/assets/css/main.css'],
\> ui: {
\> theme: {
\> prefix: 'tw'
\> }
\> },
\> fonts: {
\> processCSSVariables: true
\> }
\> })
\>
\> \`\`\`

This will automatically prefix all Tailwind utility classes and CSS variables in Nuxt UI component themes:

```html
<!-- Without prefix -->
<button class="px-2 py-1 text-xs hover:bg-primary/75">Button</button>

<!-- With prefix: tw -->
<button class="tw:px-2 tw:py-1 tw:text-xs tw:hover:bg-primary/75">Button</button>
```

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/styling-with-utility-classes#using-the-prefix-option
\> Learn more about using a prefix in the Tailwind CSS documentation.

### `prose`

Use the `prose` option to force the import of Nuxt UI `<Prose>` components even if `@nuxtjs/mdc` or `@nuxt/content` is not installed.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    prose: true
  }
})
```

### `mdc` `Deprecated`

Use the [`prose`](https://ui.nuxt.com/#prose) option instead.

### `content`

Use the `content` option to force the import of Nuxt UI `<Prose>` and `<UContent>` components even if `@nuxt/content` is not installed.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    content: true
  }
})
```

### `experimental.componentDetection` `4.1+`

Use the `experimental.componentDetection` option to enable automatic component detection for tree-shaking. This feature scans your source code to detect which components are actually used and only generates the necessary CSS for those components (including their dependencies).

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- Type: `boolean | string[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

**Enable automatic detection:**

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    experimental: {
      componentDetection: true
    }
  }
})
```

**Include additional components for dynamic usage:**

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    experimental: {
      componentDetection: ['Modal', 'Dropdown', 'Popover']
    }
  }
})
```

\> \[!NOTE]
\> When providing an array of component names, automatic detection is enabled and these components (along with their dependencies) are guaranteed to be included. This is useful for dynamic components like \`\<component \:is="..." />\` that can't be statically analyzed.

## Continuous releases

Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new){rel="&#x22;nofollow&#x22;"} for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

Automatic preview releases are created for all commits and PRs to the `v4` branch. Use them by replacing your package version with the specific commit hash or PR number.

```diff [package.json]
{
  "dependencies": {
-   "@nuxt/ui": "^4.0.0",
+   "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
  }
}
```

\> \[!NOTE]
\> pkg.pr.new will automatically comment on PRs with the installation URL, making it easy to test changes.


# Installation

\> \[!NOTE]
\> See: /docs/getting-started/installation/nuxt
\> Looking for the Nuxt version?

## Setup

### Add to a Vue project

Install the Nuxt UI package
\`\`\`bash
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun add @nuxt/ui tailwindcss
\`\`\`
Add the Nuxt UI Vite plugin in your vite.config.ts
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui()
]
})
\`\`\`
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: \[
laravel({
input: \['resources/js/app.ts'],
refresh: true
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false
}
}
}),
ui({
router: 'inertia'
})
]
})
\`\`\`
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
import adonisjs from '@adonisjs/vite/client'
import inertia from '@adonisjs/inertia/client'
export default defineConfig({
plugins: \[
adonisjs({
entrypoints: \['inertia/app/app.ts'],
reload: \['resources/views/\*\*/\*.edge']
}),
inertia(),
vue(),
ui({
router: 'inertia'
})
]
})
\`\`\`
\> \[!TIP]
\> Nuxt UI registers \`unplugin-auto-import\` and \`unplugin-vue-components\`, which will generate \`auto-imports.d.ts\` and \`components.d.ts\` type declaration files. You will likely want to gitignore these, and add them to your \`tsconfig\`.
\> \`\`\`json
\> {
\> "include": \["src/\*\*/\*.ts", "src/\*\*/\*.tsx", "src/\*\*/\*.vue", "auto-imports.d.ts", "components.d.ts"]
\> }
\>
\> \`\`\`
\>
\> \`\`\`bash
\> # Auto-generated type declarations
\> auto-imports.d.ts
\> components.d.ts
\>
\> \`\`\`
\> \[!TIP]
\> Internally, Nuxt UI relies on custom alias to resolve the theme types. If you're using TypeScript, you should add an alias to your \`tsconfig\` to enable auto-completion in your \`vite.config.ts\`.
\> \`\`\`json
\> {
\> "compilerOptions": {
\> "paths": {
\> "#build/ui": \[
\> "./node\_modules/.nuxt-ui/ui"
\> ]
\> }
\> }
\> }
\>
\> \`\`\`
\>
\> \`\`\`json
\> {
\> "compilerOptions": {
\> "paths": {
\> "#build/ui/\*": \[
\> "./node\_modules/.nuxt-ui/ui/\*"
\> ]
\> }
\> }
\> }
\>
\> \`\`\`
Use the Nuxt UI Vue plugin
\`\`\`ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
routes: \[],
history: createWebHistory()
})
app.use(router)
app.use(ui)
app.mount('#app')
\`\`\`
\`\`\`ts
import type { DefineComponent } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import ui from '@nuxt/ui/vue-plugin'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { createApp, h } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'Laravel x Nuxt UI'
createInertiaApp({
title: title => (title ? \`${title} - ${appName}\` : appName),
resolve: name =>
resolvePageComponent(
\`./pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('./pages/\*\*/\*.vue')
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ui)
.mount(el)
}
})
\`\`\`
\`\`\`ts
import type { DefineComponent } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import ui from '@nuxt/ui/vue-plugin'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
import { createApp, h } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'AdonisJS x Nuxt UI'
createInertiaApp({
title: title => (title ? \`${title} - ${appName}\` : appName),
resolve: name =>
resolvePageComponent(
\`../pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('../pages/\*\*/\*.vue')
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ui)
.mount(el)
}
})
\`\`\`
Import Tailwind CSS and Nuxt UI in your CSS
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\> \[!TIP]
\> Import the CSS file in your entrypoint.
\> \`\`\`ts
\> import './assets/css/main.css'
\>
\> import { createApp } from 'vue'
\> import { createRouter, createWebHistory } from 'vue-router'
\> import ui from '@nuxt/ui/vue-plugin'
\> import App from './App.vue'
\>
\> const app = createApp(App)
\>
\> const router = createRouter({
\> routes: \[],
\> history: createWebHistory()
\> })
\>
\> app.use(router)
\> app.use(ui)
\>
\> app.mount('#app')
\>
\> \`\`\`
\>
\> \`\`\`ts
\> import '../css/app.css'
\> import type { DefineComponent } from 'vue'
\> import { createInertiaApp } from '@inertiajs/vue3'
\> import ui from '@nuxt/ui/vue-plugin'
\> import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
\> import { createApp, h } from 'vue'
\>
\> const appName = import.meta.env.VITE\_APP\_NAME || 'Laravel x Nuxt UI'
\>
\> createInertiaApp({
\> title: title => (title ? \`${title} - ${appName}\` : appName),
\> resolve: name =>
\> resolvePageComponent(
\> \`./pages/${name}.vue\`,
\> import.meta.glob\<DefineComponent>('./pages/\*\*/\*.vue')
\> ),
\> setup({ el, App, props, plugin }) {
\> createApp({ render: () => h(App, props) })
\> .use(plugin)
\> .use(ui)
\> .mount(el)
\> }
\> })
\>
\> \`\`\`
\>
\> \`\`\`ts
\> import '../css/app.css'
\> import type { DefineComponent } from 'vue'
\> import { createInertiaApp } from '@inertiajs/vue3'
\> import ui from '@nuxt/ui/vue-plugin'
\> import { resolvePageComponent } from '@adonisjs/inertia/helpers'
\> import { createApp, h } from 'vue'
\>
\> const appName = import.meta.env.VITE\_APP\_NAME || 'AdonisJS x Nuxt UI'
\>
\> createInertiaApp({
\> title: title => (title ? \`${title} - ${appName}\` : appName),
\> resolve: name =>
\> resolvePageComponent(
\> \`../pages/${name}.vue\`,
\> import.meta.glob\<DefineComponent>('../pages/\*\*/\*.vue')
\> ),
\> setup({ el, App, props, plugin }) {
\> createApp({ render: () => h(App, props) })
\> .use(plugin)
\> .use(ui)
\> .mount(el)
\> }
\> })
\>
\> \`\`\`
\> \[!NOTE]
\> It's recommended to install the \[Tailwind CSS IntelliSense]\(https\://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
\> \`\`\`json
\> {
\> "files.associations": {
\> "\*.css": "tailwindcss"
\> },
\> "editor.quickSuggestions": {
\> "strings": "on"
\> },
\> "tailwindCSS.classAttributes": \["class", "ui"],
\> "tailwindCSS.experimental.classRegex": \[
\> \["\['\\"\`]\(\[^'\\"\`]\*)\['\\"\`]"]
\> ]
\> }
\>
\> \`\`\`
Wrap your app with App component
\`\`\`vue
\<template>
\<UApp>
\<RouterView />
\</UApp>
\</template>
\`\`\`
\`\`\`vue
\<template>
\<UApp>
\<!-- Your content goes here -->
\</UApp>
\</template>
\`\`\`
\`\`\`vue
\<template>
\<UApp>
\<!-- Your content goes here -->
\</UApp>
\</template>
\`\`\`
\> \[!NOTE]
\> See: /docs/components/app
\> The \`App\` component sets up global config and is required for Toast, Tooltip and programmatic overlays.
Add the isolate class to your root container
\`\`\`html
\<!DOCTYPE html>
\<html lang="en">
\<head>
\<meta charset="UTF-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\<title>Nuxt UI\</title>
\</head>
\<body>
\<div id="app" class="isolate">\</div>
\<script type="module" src="/src/main.ts">\</script>
\</body>
\</html>
\`\`\`
\`\`\`blade
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead
@vite('resources/js/app.ts')
\</head>
\<body>
\<div class="isolate">
@inertia
\</div>
\</body>
\</html>
\`\`\`
\`\`\`edge
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead()
@vite(\['inertia/app/app.ts', \`inertia/pages/${page.component}.vue\`])
\</head>
\<body>
@inertia({ class: 'isolate' })
\</body>
\</html>
\`\`\`
\> \[!NOTE]
\> This ensures styles are scoped to your app and prevents issues with overlays and stacking contexts.

### Use a Vue template

Get started with one of our [official templates](https://ui.nuxt.com/templates) by using the `Use this template` button on GitHub or the CLI:

\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue/dashboard
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue/chat
\`\`\`

\*\*Starter\*\*
A minimal template to get started with Nuxt UI.
\*\*Dashboard\*\*
A dashboard template with multi-column layout for building sophisticated admin interfaces.
\*\*Chat\*\*
An AI chatbot template to build your own chatbot powered by Vercel AI SDK.
\*\*Starter Adonis\*\*
A minimal Nuxt UI template for AdonisJS using Inertia.js.
\*\*Starter Laravel\*\*
A minimal Nuxt UI template for Laravel using Inertia.js.

## Options

You can customize Nuxt UI by providing options in your `vite.config.ts`.

### `prefix`

Use the `prefix` option to change the prefix of the components.

- Default: `U`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prefix: 'Nuxt'
    })
  ]
})
```

### `ui`

Use the `ui` option to provide configuration for Nuxt UI.

```ts [vite.config.ts] {9-14}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        colors: {
          primary: 'green',
          neutral: 'slate'
        }
      }
    })
  ]
})
```

### `colorMode`

Use the `colorMode` option to enable or disable the color mode integration from `@vueuse/core`.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      colorMode: false
    })
  ]
})
```

### `theme.colors`

Use the `theme.colors` option to define the dynamic color aliases used to generate components theme.

- Default: `['primary', 'secondary', 'success', 'info', 'warning', 'error']`{.inline,language-ts-type,shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-11}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        colors: ['primary', 'error']
      }
    })
  ]
})
```

\> \[!TIP]
\> See: /docs/getting-started/theme/design-system#colors
\> Learn more about color customization and theming in the Theme section.

### `theme.transitions`

Use the `theme.transitions` option to enable or disable transitions on components.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-11}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        transitions: false
      }
    })
  ]
})
```

\> \[!NOTE]
\> This option adds the \`transition-colors\` class on components with hover or active states.

### `theme.defaultVariants`

Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.

- Default: `{ color: 'primary', size: 'md' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-14}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        defaultVariants: {
          color: 'neutral',
          size: 'sm'
        }
      }
    })
  ]
})
```

### `theme.prefix` `4.2+`

Use the `theme.prefix` option to configure the same prefix you set on your Tailwind CSS import. This ensures Nuxt UI components use the correct prefixed utility classes and CSS variables.

\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
theme: {
prefix: 'tw'
}
})
]
})
\`\`\`
\`\`\`css
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";
\`\`\`

This will automatically prefix all Tailwind utility classes and CSS variables in Nuxt UI component themes:

```html
<!-- Without prefix -->
<button class="px-2 py-1 text-xs hover:bg-primary/75">Button</button>

<!-- With prefix: tw -->
<button class="tw:px-2 tw:py-1 tw:text-xs tw:hover:bg-primary/75">Button</button>
```

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/styling-with-utility-classes#using-the-prefix-option
\> Learn more about using a prefix in the Tailwind CSS documentation.

### `prose`

Use the `prose` option to enable Nuxt UI `<Prose>` components and their theme.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prose: true
    })
  ]
})
```

### `autoImport`

Use the `autoImport` option to disable composable auto-imports or to customize [`unplugin-auto-import`](https://github.com/unplugin/unplugin-auto-import){rel="&#x22;nofollow&#x22;"} options.

- Default: `{}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      autoImport: false
    })
  ]
})
```

\> \[!NOTE]
\> When disabled, you can still import composables explicitly from \`@nuxt/ui/composables\`.

### `components`

Use the `components` option to disable component auto-imports or to customize [`unplugin-vue-components`](https://github.com/unplugin/unplugin-vue-components){rel="&#x22;nofollow&#x22;"} options.

- Default: `{}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      components: false
    })
  ]
})
```

\> \[!NOTE]
\> When disabled, you can still import components explicitly, e.g. \`import Button from '@nuxt/ui/components/Button.vue'\` or \`import ProseCode from '@nuxt/ui/components/prose/Code.vue'\`.

### `router` `4.3+`

Use the `router` option to configure routing integration. This is useful for applications that don't use `vue-router`, such as Electron apps, MPAs, or frameworks like [Inertia.js](https://inertiajs.com/){rel="&#x22;nofollow&#x22;"} or [Hybridly](https://hybridly.dev/){rel="&#x22;nofollow&#x22;"}.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

| Value                                                                                                                           | Description                                                     |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `true`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}      | Uses `vue-router` for navigation with `RouterLink` component.   |
| `false`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}     | Disables routing integration, links render as plain `<a>` tags. |
| `'inertia'`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | Uses Inertia.js for navigation with its `Link` component.       |

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      router: false
    })
  ]
})
```

\> \[!TIP]
\> You can provide custom navigation logic for frameworks like Hybridly by setting \`router: false\` in the Vite config and passing a function when installing the Vue plugin:
\> \`\`\`ts
\> import ui from '@nuxt/ui/vue-plugin'
\> import { router } from 'hybridly'
\>
\> app.use(ui, {
\> router: (event, { href, external }) => {
\> if (external) {
\> return
\> }
\>
\> event.preventDefault()
\>
\> router.navigate({ url: href })
\> }
\> })
\>
\> \`\`\`

\> \[!NOTE]
\> When set to \`false\` or \`'inertia'\`, \`vue-router\` is not required as a dependency.

### `scanPackages` `4.3+`

Use the `scanPackages` option to specify additional npm packages that should be scanned for components using Nuxt UI. This is useful when you have a shared component library that uses Nuxt UI components internally.

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      scanPackages: ['@my-org/ui-components']
    })
  ]
})
```

\> \[!NOTE]
\> By default, only \`@nuxt/ui\` is scanned. Use this option when your external packages contain Vue components that use Nuxt UI.

## Continuous releases

Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new){rel="&#x22;nofollow&#x22;"} for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

Automatic preview releases are created for all commits and PRs to the `v4` branch. Use them by replacing your package version with the specific commit hash or PR number.

```diff [package.json]
{
  "dependencies": {
-   "@nuxt/ui": "^4.0.0",
+   "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
  }
}
```

\> \[!NOTE]
\> pkg.pr.new will automatically comment on PRs with the installation URL, making it easy to test changes.


# Introduction

## What is Nuxt UI?

A modern Vue UI component library built on [Reka UI](https://reka-ui.com/){rel="&#x22;nofollow&#x22;"}, [Tailwind CSS](https://tailwindcss.com/){rel="&#x22;nofollow&#x22;"}, and [Tailwind Variants](https://www.tailwind-variants.org/){rel="&#x22;nofollow&#x22;"} to ship beautiful and accessible applications with 125+ production-ready components. Works with Nuxt and plain Vue apps (Vite, Inertia, SSR).

If you're building a Vue project with Tailwind CSS, Nuxt UI is a great default choice. It provides high-level, ready-to-use components (data tables, forms, overlays, navigation) while still allowing advanced customization when needed.

\*\*Developer Experience First\*\*
Intuitive APIs, excellent TypeScript support, auto-completion, and comprehensive docs.
\*\*Beautiful by Default\*\*
A modern, clean design out of the box with a theme you can adapt in minutes.
\*\*Accessible by Default\*\*
WAI-ARIA compliant with keyboard navigation, focus management, and screen reader support.
\*\*Production Ready\*\*
125+ battle-tested components including data tables, forms, overlays, and navigation used by thousands of applications in production.

## What's new in v4?

Nuxt UI v4 marks a major milestone: Nuxt UI and Nuxt UI Pro are now unified into a single, fully open-source and free library of 125+ production-ready components and a complete Figma Kit.

The migration from v3 to v4 will be much smoother than from v2 to v3. Read more in the [migration guide](https://ui.nuxt.com/docs/getting-started/migration/v4).

\> \[!NOTE]
\> See: /docs/getting-started/migration/v3
\> If you are migrating from v2, you can read more in this migration guide.

## Core technologies

### Reka UI

Nuxt UI is built on top of [Reka UI](https://reka-ui.com/){rel="&#x22;nofollow&#x22;"} as a foundation for the components:

- **WAI-ARIA Compliance**: Follows [WAI-ARIA authoring practices](https://reka-ui.com/docs/overview/accessibility){rel="&#x22;nofollow&#x22;"} with proper semantics and roles
- **Keyboard Navigation**: Built-in keyboard support for complex components like tabs and dialogs
- **Focus Management**: Intelligent focus handling that moves focus based on user interactions
- **Accessible Labels**: Abstractions to simplify labeling controls for screen readers

### Tailwind CSS

Nuxt UI integrates the latest [Tailwind CSS](https://tailwindcss.com/){rel="&#x22;nofollow&#x22;"}, bringing significant improvements:

- **5x Faster Builds**: Full builds up to 5x faster, incremental builds over 100x faster
- **Unified Toolchain**: Built-in import handling, vendor prefixing, and syntax transforms
- **CSS-first Configuration**: Customize and extend directly in CSS instead of JavaScript
- **Modern Web Features**: Container queries, cascade layers, wide-gamut colors, and more

### Tailwind Variants

Nuxt UI takes advantage of [Tailwind Variants](https://www.tailwind-variants.org/){rel="&#x22;nofollow&#x22;"} to provide a powerful design system:

- **Dynamic Styling**: Flexible component variants with a powerful API
- **Type Safety**: Full TypeScript support with auto-completion
- **Conflict Resolution**: Efficient merging of conflicting styles

## Key features

### Ecosystem integration

Nuxt UI is SSR compatible and integrates seamlessly with the Nuxt ecosystem (these features also work in Vue with additional configuration):

- [**Icons**](https://ui.nuxt.com/docs/getting-started/integrations/icons): Access 200,000+ icons from Iconify
- [**Fonts**](https://ui.nuxt.com/docs/getting-started/integrations/fonts): Plug-and-play web font optimization and configuration
- [**Color Mode**](https://ui.nuxt.com/docs/getting-started/integrations/color-mode): Dark and Light mode with auto detection
- [**i18n**](https://ui.nuxt.com/docs/getting-started/integrations/i18n): Internationalize your components with 50+ languages
- [**Content**](https://ui.nuxt.com/docs/getting-started/integrations/content): Beautiful typography out of the box

### Vue compatibility (Nuxt optional)

Nuxt UI works with any Vue project, not just Nuxt. Simply add the Vite and Vue plugins to your configuration:

- **Auto-imports**: Components and composables are automatically imported and available globally
- **Design System**: Full theming support with customizable colors, sizes, variants, and more
- **Developer Experience**: Complete TypeScript support with IntelliSense and auto-completion

\> \[!TIP]
\> See: /docs/getting-started/installation/vue
\> Learn how to install and configure Nuxt UI in a Vue project in the Vue installation guide.

### TypeScript Support

Nuxt UI provides comprehensive TypeScript integration for a superior developer experience:

- **Auto-completion**: For all component props, slots, and events
- **Generic Components**: Using [Vue Generics](https://vuejs.org/api/sfc-script-setup.html#generics){rel="&#x22;nofollow&#x22;"}
- **Type-safe Theming**: In `app.config.ts`
- **IntelliSense**: Throughout your entire codebase

### Templates

Nuxt UI provides production-ready templates for both Nuxt and Vue to help you get started quickly:

- **Nuxt templates**: Dashboard, SaaS, Landing, Docs, Portfolio, Chat, Editor, Changelog, and Starter
- **Vue templates**: Dashboard and Starter

\> \[!TIP]
\> See: /templates
\> Explore all available templates to kickstart your next project.

## FAQ

\*\*Q: Is Nuxt UI free to use?\*\*
A: Yes! Nuxt UI is completely free and open source under the MIT license. All 125+ components are available to everyone.
\*\*Q: Can I use Nuxt UI with Vue without Nuxt?\*\*
A: Yes! While optimized for Nuxt, Nuxt UI works perfectly with standalone Vue projects via our Vite plugin. You can follow the \[installation guide]\(/docs/getting-started/installation/vue) to get started.
\*\*Q: Does Nuxt UI include a Figma Kit?\*\*
A: Yes! Nuxt UI includes a \[complete Figma Kit]\(https\://go.nuxt.com/figma-ui) with all components, making it easy for designers and developers to collaborate.
\*\*Q: How does Nuxt UI handle accessibility?\*\*
A: Through \[Reka UI]\(https\://reka-ui.com/docs/overview/accessibility) integration, Nuxt UI provides automatic ARIA attributes, keyboard navigation, focus management, and screen reader support. While offering a strong foundation, testing in your specific use case remains important.
\*\*Q: Is Nuxt UI production-ready?\*\*
A: Yes! Nuxt UI is used in production by thousands of applications with 1000+ Vitest tests, regular updates, and active maintenance.
\*\*Q: When should I consider alternatives?\*\*
A: Consider Vuetify if you want Material Design styling, ant-design-vue for Ant Design styling, PrimeVue or Element Plus if you don't want Tailwind CSS, shadcn-vue if you prefer copying components into your repo, Quasar for cross-platform apps (web, mobile, desktop), or Reka UI / Headless UI if you only need unstyled primitives.
\*\*Q: Where can I get help?\*\*
A: Join our \[Discord community]\(https\://go.nuxt.com/discord) for discussions or report issues on \[GitHub]\(https\://github.com/nuxt/ui/issues).


# Installation

\> \[!NOTE]
\> See: /docs/getting-started/installation/vue
\> Looking for the Vue version?

## Setup

### Add to a Nuxt project

Install the Nuxt UI package
\`\`\`bash
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun add @nuxt/ui tailwindcss
\`\`\`
Add the Nuxt UI module in your nuxt.config.ts
\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui']
})
\`\`\`
Import Tailwind CSS and Nuxt UI in your CSS
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui'],
css: \['\~/assets/css/main.css']
})
\`\`\`
\> \[!TIP]
\> See: https\://nuxt.com/docs/getting-started/layers
\> When using \[Nuxt Layers]\(https\://nuxt.com/docs/getting-started/layers), the module automatically generates \[\`@source\`]\(https\://tailwindcss.com/docs/functions-and-directives#source-directive) directives for each layer directory, ensuring Tailwind CSS scans all your layer source files for utility classes.
\> \[!NOTE]
\> It's recommended to install the \[Tailwind CSS IntelliSense]\(https\://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
\> \`\`\`json
\> {
\> "files.associations": {
\> "\*.css": "tailwindcss"
\> },
\> "editor.quickSuggestions": {
\> "strings": "on"
\> },
\> "tailwindCSS.classAttributes": \["class", "ui"],
\> "tailwindCSS.experimental.classRegex": \[
\> \["\['\\"\`]\(\[^'\\"\`]\*)\['\\"\`]"]
\> ]
\> }
\>
\> \`\`\`
Wrap your app with App component
\`\`\`vue
\<template>
\<UApp>
\<NuxtPage />
\</UApp>
\</template>
\`\`\`
\> \[!NOTE]
\> See: /docs/components/app
\> The \`App\` component provides global configurations and is required for Toast, Tooltip components to work as well as Programmatic Overlays.

### Use a Nuxt template

Get started with one of our [official templates](https://ui.nuxt.com/templates) by using the `Use this template` button on GitHub or the CLI:

\`\`\`bash
npm create nuxt\@latest -- -t ui
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/landing
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/docs
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/saas
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/dashboard
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/chat
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/portfolio
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/changelog
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- -t ui/editor
\`\`\`

\*\*Starter\*\*
A minimal template to get started with Nuxt UI.
\*\*Landing\*\*
A modern landing page template powered by Nuxt Content.
\*\*Docs\*\*
A documentation template powered by Nuxt Content.
\*\*SaaS\*\*
A SaaS template with landing, pricing, docs and blog powered by Nuxt Content.
\*\*Dashboard\*\*
A dashboard template with multi-column layout for building sophisticated admin interfaces.
\*\*Chat\*\*
An AI chatbot template to build your own chatbot powered by Vercel AI SDK.
\*\*Portfolio\*\*
A sleek portfolio template to showcase your work, skills and blog powered by Nuxt Content.
\*\*Changelog\*\*
A changelog template to display your repository releases notes from GitHub powered by Nuxt MDC.
\*\*Editor\*\*
A rich text editor template powered by TipTap with support for markdown, HTML, and JSON content types.

## Options

You can customize Nuxt UI by providing options in your `nuxt.config.ts`.

### `prefix`

Use the `prefix` option to change the prefix of the components.

- Default: `U`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    prefix: 'Nuxt'
  }
})
```

### `fonts`

Use the `fonts` option to enable or disable the [`@nuxt/fonts`](https://github.com/nuxt/fonts){rel="&#x22;nofollow&#x22;"} module.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    fonts: false
  }
})
```

### `colorMode`

Use the `colorMode` option to enable or disable the [`@nuxt/color-mode`](https://github.com/nuxt-modules/color-mode){rel="&#x22;nofollow&#x22;"} module.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    colorMode: false
  }
})
```

### `theme.colors`

Use the `theme.colors` option to define the dynamic color aliases used to generate components theme.

- Default: `['primary', 'secondary', 'success', 'info', 'warning', 'error']`{.inline,language-ts-type,shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      colors: ['primary', 'error']
    }
  }
})
```

\> \[!TIP]
\> See: /docs/getting-started/theme/design-system#colors
\> Learn more about color customization and theming in the Theme section.

### `theme.transitions`

Use the `theme.transitions` option to enable or disable transitions on components.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      transitions: false
    }
  }
})
```

\> \[!NOTE]
\> This option adds the \`transition-colors\` class on components with hover or active states.

### `theme.defaultVariants`

Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.

- Default: `{ color: 'primary', size: 'md' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-11}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    theme: {
      defaultVariants: {
        color: 'neutral',
        size: 'sm'
      }
    }
  }
})
```

### `theme.prefix` `4.2+`

Use the `theme.prefix` option to configure the same prefix you set on your Tailwind CSS import. This ensures Nuxt UI components use the correct prefixed utility classes and CSS variables.

\`\`\`ts
export default defineNuxtConfig({
modules: \['@nuxt/ui'],
css: \['\~/assets/css/main.css'],
ui: {
theme: {
prefix: 'tw'
}
}
})
\`\`\`
\`\`\`css
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";
\`\`\`

\> \[!WARNING]
\> See: https\://fonts.nuxt.com/get-started/configuration#processcssvariables
\> You might need to enable \`fonts.processCSSVariables\` to use the prefix option with the \`@nuxt/fonts\` module:
\> \`\`\`ts
\> export default defineNuxtConfig({
\> modules: \['@nuxt/ui'],
\> css: \['\~/assets/css/main.css'],
\> ui: {
\> theme: {
\> prefix: 'tw'
\> }
\> },
\> fonts: {
\> processCSSVariables: true
\> }
\> })
\>
\> \`\`\`

This will automatically prefix all Tailwind utility classes and CSS variables in Nuxt UI component themes:

```html
<!-- Without prefix -->
<button class="px-2 py-1 text-xs hover:bg-primary/75">Button</button>

<!-- With prefix: tw -->
<button class="tw:px-2 tw:py-1 tw:text-xs tw:hover:bg-primary/75">Button</button>
```

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/styling-with-utility-classes#using-the-prefix-option
\> Learn more about using a prefix in the Tailwind CSS documentation.

### `prose`

Use the `prose` option to force the import of Nuxt UI `<Prose>` components even if `@nuxtjs/mdc` or `@nuxt/content` is not installed.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    prose: true
  }
})
```

### `mdc` `Deprecated`

Use the [`prose`](https://ui.nuxt.com/#prose) option instead.

### `content`

Use the `content` option to force the import of Nuxt UI `<Prose>` and `<UContent>` components even if `@nuxt/content` is not installed.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [nuxt.config.ts] {4-6}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    content: true
  }
})
```

### `experimental.componentDetection` `4.1+`

Use the `experimental.componentDetection` option to enable automatic component detection for tree-shaking. This feature scans your source code to detect which components are actually used and only generates the necessary CSS for those components (including their dependencies).

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- Type: `boolean | string[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

**Enable automatic detection:**

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    experimental: {
      componentDetection: true
    }
  }
})
```

**Include additional components for dynamic usage:**

```ts [nuxt.config.ts] {4-8}
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    experimental: {
      componentDetection: ['Modal', 'Dropdown', 'Popover']
    }
  }
})
```

\> \[!NOTE]
\> When providing an array of component names, automatic detection is enabled and these components (along with their dependencies) are guaranteed to be included. This is useful for dynamic components like \`\<component \:is="..." />\` that can't be statically analyzed.

## Continuous releases

Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new){rel="&#x22;nofollow&#x22;"} for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

Automatic preview releases are created for all commits and PRs to the `v4` branch. Use them by replacing your package version with the specific commit hash or PR number.

```diff [package.json]
{
  "dependencies": {
-   "@nuxt/ui": "^4.0.0",
+   "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
  }
}
```

\> \[!NOTE]
\> pkg.pr.new will automatically comment on PRs with the installation URL, making it easy to test changes.


# Installation

\> \[!NOTE]
\> See: /docs/getting-started/installation/nuxt
\> Looking for the Nuxt version?

## Setup

### Add to a Vue project

Install the Nuxt UI package
\`\`\`bash
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun add @nuxt/ui tailwindcss
\`\`\`
Add the Nuxt UI Vite plugin in your vite.config.ts
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui()
]
})
\`\`\`
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: \[
laravel({
input: \['resources/js/app.ts'],
refresh: true
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false
}
}
}),
ui({
router: 'inertia'
})
]
})
\`\`\`
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
import adonisjs from '@adonisjs/vite/client'
import inertia from '@adonisjs/inertia/client'
export default defineConfig({
plugins: \[
adonisjs({
entrypoints: \['inertia/app/app.ts'],
reload: \['resources/views/\*\*/\*.edge']
}),
inertia(),
vue(),
ui({
router: 'inertia'
})
]
})
\`\`\`
\> \[!TIP]
\> Nuxt UI registers \`unplugin-auto-import\` and \`unplugin-vue-components\`, which will generate \`auto-imports.d.ts\` and \`components.d.ts\` type declaration files. You will likely want to gitignore these, and add them to your \`tsconfig\`.
\> \`\`\`json
\> {
\> "include": \["src/\*\*/\*.ts", "src/\*\*/\*.tsx", "src/\*\*/\*.vue", "auto-imports.d.ts", "components.d.ts"]
\> }
\>
\> \`\`\`
\>
\> \`\`\`bash
\> # Auto-generated type declarations
\> auto-imports.d.ts
\> components.d.ts
\>
\> \`\`\`
\> \[!TIP]
\> Internally, Nuxt UI relies on custom alias to resolve the theme types. If you're using TypeScript, you should add an alias to your \`tsconfig\` to enable auto-completion in your \`vite.config.ts\`.
\> \`\`\`json
\> {
\> "compilerOptions": {
\> "paths": {
\> "#build/ui": \[
\> "./node\_modules/.nuxt-ui/ui"
\> ]
\> }
\> }
\> }
\>
\> \`\`\`
\>
\> \`\`\`json
\> {
\> "compilerOptions": {
\> "paths": {
\> "#build/ui/\*": \[
\> "./node\_modules/.nuxt-ui/ui/\*"
\> ]
\> }
\> }
\> }
\>
\> \`\`\`
Use the Nuxt UI Vue plugin
\`\`\`ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
routes: \[],
history: createWebHistory()
})
app.use(router)
app.use(ui)
app.mount('#app')
\`\`\`
\`\`\`ts
import type { DefineComponent } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import ui from '@nuxt/ui/vue-plugin'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { createApp, h } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'Laravel x Nuxt UI'
createInertiaApp({
title: title => (title ? \`${title} - ${appName}\` : appName),
resolve: name =>
resolvePageComponent(
\`./pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('./pages/\*\*/\*.vue')
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ui)
.mount(el)
}
})
\`\`\`
\`\`\`ts
import type { DefineComponent } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import ui from '@nuxt/ui/vue-plugin'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
import { createApp, h } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'AdonisJS x Nuxt UI'
createInertiaApp({
title: title => (title ? \`${title} - ${appName}\` : appName),
resolve: name =>
resolvePageComponent(
\`../pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('../pages/\*\*/\*.vue')
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ui)
.mount(el)
}
})
\`\`\`
Import Tailwind CSS and Nuxt UI in your CSS
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
\> \[!TIP]
\> Import the CSS file in your entrypoint.
\> \`\`\`ts
\> import './assets/css/main.css'
\>
\> import { createApp } from 'vue'
\> import { createRouter, createWebHistory } from 'vue-router'
\> import ui from '@nuxt/ui/vue-plugin'
\> import App from './App.vue'
\>
\> const app = createApp(App)
\>
\> const router = createRouter({
\> routes: \[],
\> history: createWebHistory()
\> })
\>
\> app.use(router)
\> app.use(ui)
\>
\> app.mount('#app')
\>
\> \`\`\`
\>
\> \`\`\`ts
\> import '../css/app.css'
\> import type { DefineComponent } from 'vue'
\> import { createInertiaApp } from '@inertiajs/vue3'
\> import ui from '@nuxt/ui/vue-plugin'
\> import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
\> import { createApp, h } from 'vue'
\>
\> const appName = import.meta.env.VITE\_APP\_NAME || 'Laravel x Nuxt UI'
\>
\> createInertiaApp({
\> title: title => (title ? \`${title} - ${appName}\` : appName),
\> resolve: name =>
\> resolvePageComponent(
\> \`./pages/${name}.vue\`,
\> import.meta.glob\<DefineComponent>('./pages/\*\*/\*.vue')
\> ),
\> setup({ el, App, props, plugin }) {
\> createApp({ render: () => h(App, props) })
\> .use(plugin)
\> .use(ui)
\> .mount(el)
\> }
\> })
\>
\> \`\`\`
\>
\> \`\`\`ts
\> import '../css/app.css'
\> import type { DefineComponent } from 'vue'
\> import { createInertiaApp } from '@inertiajs/vue3'
\> import ui from '@nuxt/ui/vue-plugin'
\> import { resolvePageComponent } from '@adonisjs/inertia/helpers'
\> import { createApp, h } from 'vue'
\>
\> const appName = import.meta.env.VITE\_APP\_NAME || 'AdonisJS x Nuxt UI'
\>
\> createInertiaApp({
\> title: title => (title ? \`${title} - ${appName}\` : appName),
\> resolve: name =>
\> resolvePageComponent(
\> \`../pages/${name}.vue\`,
\> import.meta.glob\<DefineComponent>('../pages/\*\*/\*.vue')
\> ),
\> setup({ el, App, props, plugin }) {
\> createApp({ render: () => h(App, props) })
\> .use(plugin)
\> .use(ui)
\> .mount(el)
\> }
\> })
\>
\> \`\`\`
\> \[!NOTE]
\> It's recommended to install the \[Tailwind CSS IntelliSense]\(https\://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
\> \`\`\`json
\> {
\> "files.associations": {
\> "\*.css": "tailwindcss"
\> },
\> "editor.quickSuggestions": {
\> "strings": "on"
\> },
\> "tailwindCSS.classAttributes": \["class", "ui"],
\> "tailwindCSS.experimental.classRegex": \[
\> \["\['\\"\`]\(\[^'\\"\`]\*)\['\\"\`]"]
\> ]
\> }
\>
\> \`\`\`
Wrap your app with App component
\`\`\`vue
\<template>
\<UApp>
\<RouterView />
\</UApp>
\</template>
\`\`\`
\`\`\`vue
\<template>
\<UApp>
\<!-- Your content goes here -->
\</UApp>
\</template>
\`\`\`
\`\`\`vue
\<template>
\<UApp>
\<!-- Your content goes here -->
\</UApp>
\</template>
\`\`\`
\> \[!NOTE]
\> See: /docs/components/app
\> The \`App\` component sets up global config and is required for Toast, Tooltip and programmatic overlays.
Add the isolate class to your root container
\`\`\`html
\<!DOCTYPE html>
\<html lang="en">
\<head>
\<meta charset="UTF-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\<title>Nuxt UI\</title>
\</head>
\<body>
\<div id="app" class="isolate">\</div>
\<script type="module" src="/src/main.ts">\</script>
\</body>
\</html>
\`\`\`
\`\`\`blade
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead
@vite('resources/js/app.ts')
\</head>
\<body>
\<div class="isolate">
@inertia
\</div>
\</body>
\</html>
\`\`\`
\`\`\`edge
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead()
@vite(\['inertia/app/app.ts', \`inertia/pages/${page.component}.vue\`])
\</head>
\<body>
@inertia({ class: 'isolate' })
\</body>
\</html>
\`\`\`
\> \[!NOTE]
\> This ensures styles are scoped to your app and prevents issues with overlays and stacking contexts.

### Use a Vue template

Get started with one of our [official templates](https://ui.nuxt.com/templates) by using the `Use this template` button on GitHub or the CLI:

\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue/dashboard
\`\`\`
\`\`\`bash
npm create nuxt\@latest -- --no-modules -t ui-vue/chat
\`\`\`

\*\*Starter\*\*
A minimal template to get started with Nuxt UI.
\*\*Dashboard\*\*
A dashboard template with multi-column layout for building sophisticated admin interfaces.
\*\*Chat\*\*
An AI chatbot template to build your own chatbot powered by Vercel AI SDK.
\*\*Starter Adonis\*\*
A minimal Nuxt UI template for AdonisJS using Inertia.js.
\*\*Starter Laravel\*\*
A minimal Nuxt UI template for Laravel using Inertia.js.

## Options

You can customize Nuxt UI by providing options in your `vite.config.ts`.

### `prefix`

Use the `prefix` option to change the prefix of the components.

- Default: `U`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prefix: 'Nuxt'
    })
  ]
})
```

### `ui`

Use the `ui` option to provide configuration for Nuxt UI.

```ts [vite.config.ts] {9-14}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        colors: {
          primary: 'green',
          neutral: 'slate'
        }
      }
    })
  ]
})
```

### `colorMode`

Use the `colorMode` option to enable or disable the color mode integration from `@vueuse/core`.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      colorMode: false
    })
  ]
})
```

### `theme.colors`

Use the `theme.colors` option to define the dynamic color aliases used to generate components theme.

- Default: `['primary', 'secondary', 'success', 'info', 'warning', 'error']`{.inline,language-ts-type,shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-11}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        colors: ['primary', 'error']
      }
    })
  ]
})
```

\> \[!TIP]
\> See: /docs/getting-started/theme/design-system#colors
\> Learn more about color customization and theming in the Theme section.

### `theme.transitions`

Use the `theme.transitions` option to enable or disable transitions on components.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-11}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        transitions: false
      }
    })
  ]
})
```

\> \[!NOTE]
\> This option adds the \`transition-colors\` class on components with hover or active states.

### `theme.defaultVariants`

Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.

- Default: `{ color: 'primary', size: 'md' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9-14}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      theme: {
        defaultVariants: {
          color: 'neutral',
          size: 'sm'
        }
      }
    })
  ]
})
```

### `theme.prefix` `4.2+`

Use the `theme.prefix` option to configure the same prefix you set on your Tailwind CSS import. This ensures Nuxt UI components use the correct prefixed utility classes and CSS variables.

\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
theme: {
prefix: 'tw'
}
})
]
})
\`\`\`
\`\`\`css
@import "tailwindcss" prefix(tw);
@import "@nuxt/ui";
\`\`\`

This will automatically prefix all Tailwind utility classes and CSS variables in Nuxt UI component themes:

```html
<!-- Without prefix -->
<button class="px-2 py-1 text-xs hover:bg-primary/75">Button</button>

<!-- With prefix: tw -->
<button class="tw:px-2 tw:py-1 tw:text-xs tw:hover:bg-primary/75">Button</button>
```

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/styling-with-utility-classes#using-the-prefix-option
\> Learn more about using a prefix in the Tailwind CSS documentation.

### `prose`

Use the `prose` option to enable Nuxt UI `<Prose>` components and their theme.

- Default: `false`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prose: true
    })
  ]
})
```

### `autoImport`

Use the `autoImport` option to disable composable auto-imports or to customize [`unplugin-auto-import`](https://github.com/unplugin/unplugin-auto-import){rel="&#x22;nofollow&#x22;"} options.

- Default: `{}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      autoImport: false
    })
  ]
})
```

\> \[!NOTE]
\> When disabled, you can still import composables explicitly from \`@nuxt/ui/composables\`.

### `components`

Use the `components` option to disable component auto-imports or to customize [`unplugin-vue-components`](https://github.com/unplugin/unplugin-vue-components){rel="&#x22;nofollow&#x22;"} options.

- Default: `{}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      components: false
    })
  ]
})
```

\> \[!NOTE]
\> When disabled, you can still import components explicitly, e.g. \`import Button from '@nuxt/ui/components/Button.vue'\` or \`import ProseCode from '@nuxt/ui/components/prose/Code.vue'\`.

### `router` `4.3+`

Use the `router` option to configure routing integration. This is useful for applications that don't use `vue-router`, such as Electron apps, MPAs, or frameworks like [Inertia.js](https://inertiajs.com/){rel="&#x22;nofollow&#x22;"} or [Hybridly](https://hybridly.dev/){rel="&#x22;nofollow&#x22;"}.

- Default: `true`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

| Value                                                                                                                           | Description                                                     |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `true`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}      | Uses `vue-router` for navigation with `RouterLink` component.   |
| `false`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}     | Disables routing integration, links render as plain `<a>` tags. |
| `'inertia'`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | Uses Inertia.js for navigation with its `Link` component.       |

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      router: false
    })
  ]
})
```

\> \[!TIP]
\> You can provide custom navigation logic for frameworks like Hybridly by setting \`router: false\` in the Vite config and passing a function when installing the Vue plugin:
\> \`\`\`ts
\> import ui from '@nuxt/ui/vue-plugin'
\> import { router } from 'hybridly'
\>
\> app.use(ui, {
\> router: (event, { href, external }) => {
\> if (external) {
\> return
\> }
\>
\> event.preventDefault()
\>
\> router.navigate({ url: href })
\> }
\> })
\>
\> \`\`\`

\> \[!NOTE]
\> When set to \`false\` or \`'inertia'\`, \`vue-router\` is not required as a dependency.

### `scanPackages` `4.3+`

Use the `scanPackages` option to specify additional npm packages that should be scanned for components using Nuxt UI. This is useful when you have a shared component library that uses Nuxt UI components internally.

```ts [vite.config.ts] {9}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      scanPackages: ['@my-org/ui-components']
    })
  ]
})
```

\> \[!NOTE]
\> By default, only \`@nuxt/ui\` is scanned. Use this option when your external packages contain Vue components that use Nuxt UI.

## Continuous releases

Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new){rel="&#x22;nofollow&#x22;"} for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

Automatic preview releases are created for all commits and PRs to the `v4` branch. Use them by replacing your package version with the specific commit hash or PR number.

```diff [package.json]
{
  "dependencies": {
-   "@nuxt/ui": "^4.0.0",
+   "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
  }
}
```

\> \[!NOTE]
\> pkg.pr.new will automatically comment on PRs with the installation URL, making it easy to test changes.


# Migration to v4

Nuxt UI v4 marks a major milestone: **Nuxt UI and Nuxt UI Pro are now unified into a single, fully open-source and free library**. You now have access to 125+ production-ready components, all available in the `@nuxt/ui` package.

\> \[!NOTE]
\> Nuxt UI v4 requires Nuxt 4 due to some dependencies. Make sure to upgrade to Nuxt 4 before migrating to Nuxt UI v4.

This guide provides step-by-step instructions to migrate your application from v3 to v4.

## Migrate your project

### From Nuxt UI Pro

1. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your `package.json`:

\`\`\`bash
pnpm remove @nuxt/ui-pro
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn remove @nuxt/ui-pro
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm uninstall @nuxt/ui-pro
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun remove @nuxt/ui-pro
bun add @nuxt/ui tailwindcss
\`\`\`

\*\*Nuxt:\*\*
Replace @nuxt/ui-pro with @nuxt/ui in your nuxt.config.ts:
\`\`\`diff
export default defineNuxtConfig({
modules: \[
\- '@nuxt/ui-pro',
\+ '@nuxt/ui'
]
})
\`\`\`
\*\*Vue:\*\*
Replace @nuxt/ui-pro with @nuxt/ui in your vite.config.ts:
\`\`\`diff
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
\- import uiPro from '@nuxt/ui-pro/vite'
\+ import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
\- uiPro({
\+ ui({
ui: {
colors: {
primary: 'green',
neutral: 'slate'
}
}
})
]
})
\`\`\`

\*\*Nuxt:\*\*
Use the ui key instead of uiPro in your app.config.ts:
\`\`\`diff
export default defineAppConfig({
ui: {
colors: {
primary: 'green',
neutral: 'slate'
},
\+ pageCard: {
\+ slots: {
\+ root: 'rounded-xl',
\+ }
\+ }
},
\- uiPro: {
\- pageCard: {
\- slots: {
\- root: 'rounded-xl',
\- }
\- }
\- }
})
\`\`\`
\*\*Vue:\*\*
Use the ui key instead of uiPro in your vite.config.ts:
\`\`\`diff
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
colors: {
primary: 'green',
neutral: 'slate'
},
\+ pageCard: {
\+ slots: {
\+ root: 'rounded-xl',
\+ }
\+ }
},
\- uiPro: {
\- pageCard: {
\- slots: {
\- root: 'rounded-xl',
\- }
\- }
\- }
})
]
})
\`\`\`

4. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your CSS:

\*\*Nuxt:\*\*
\`\`\`diff
@import "tailwindcss";
\- @import "@nuxt/ui-pro";
\+ @import "@nuxt/ui";
\`\`\`
\> \[!WARNING]
\> If you are upgrading to Nuxt 4 at the same time as Nuxt UI v4, make sure to update the \`@source\` directive to match the new directory structure.
\> \`\`\`diff
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> - @source "../../content/\*\*/\*";
\> + @source "../../../content/\*\*/\*";
\>
\> \`\`\`
\*\*Vue:\*\*
\`\`\`diff
@import "tailwindcss";
\- @import "@nuxt/ui-pro";
\+ @import "@nuxt/ui";
\`\`\`

5. Replace `@nuxt/ui-pro` with `@nuxt/ui` in your imports:

```diff
- import type { BannerProps } from '@nuxt/ui-pro'
+ import type { BannerProps } from '@nuxt/ui'
```

### From Nuxt UI

1. When upgrading from Nuxt UI v3, you simply need to update to v4:

\`\`\`bash
pnpm add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
yarn add @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
npm install @nuxt/ui tailwindcss
\`\`\`
\`\`\`bash
bun add @nuxt/ui tailwindcss
\`\`\`

## Changes from v3

After upgrading to Nuxt UI v4, please note the following important changes:

### Renamed ButtonGroup

The `ButtonGroup` component has been renamed to [`FieldGroup`](https://ui.nuxt.com/docs/components/field-group):

```diff
<template>
- <UButtonGroup>
+ <UFieldGroup>
    <UButton label="Button" />
    <UButton icon="i-lucide-chevron-down" />
+ </UFieldGroup>
- </UButtonGroup>
</template>
```

### Renamed PageMarquee

The `PageMarquee` component has been renamed to [`Marquee`](https://ui.nuxt.com/docs/components/marquee):

```diff
<template>
- <UPageMarquee :items="items" />
+ <UMarquee :items="items" />
</template>
```

### Removed PageAccordion

The `PageAccordion` component has been removed in favor of [`Accordion`](https://ui.nuxt.com/docs/components/accordion):

```diff
<template>
- <UPageAccordion
+ <UAccordion
    :items="items"
+   :unmount-on-hide="false"
+   :ui="{ trigger: 'text-base', body: 'text-base text-muted' }"
  />
</template>
```

\> \[!NOTE]
\> The \`PageAccordion\` component was a wrapper that set \`unmount-on-hide\` to \`false\` and customized the \`ui\` prop.

### Renamed model modifiers

The `modelModifiers` shape used by [`Input`](https://ui.nuxt.com/docs/components/input), [`InputNumber`](https://ui.nuxt.com/docs/components/input-number) and [`Textarea`](https://ui.nuxt.com/docs/components/textarea) has changed in v4:

1. The `nullify` modifier was renamed to `nullable` (it converts empty/blank values to `null`).
2. A new `optional` modifier was added (it converts empty/blank values to `undefined`).

```diff
- <UInput v-model.nullify="value" />
+ <UInput v-model.nullable="value" />
```

```diff
- <UTextarea v-model="value" :model-modifiers="{ nullify: true }" />
+ <UTextarea v-model="value" :model-modifiers="{ nullable: true }" />
```

Use `nullable` when you want empty values as `null`, and `optional` when you prefer `undefined` for absent values.

### Changes to Form component

The `Form` component has been improved in v4 with better state management and nested form handling. Here are the key changes you need to be aware of:

1. Schema **transformations will only*&#x2A; be applied to the **`@submit` data** and will no longer mutate the form's state. This provides better predictability and prevents unexpected state mutations.
2. **Nested forms must be enabled explicitly** using the `nested` prop. This makes the component behavior more explicit and prevents accidental nested form creation.
3. **Nested forms should now provide a `name`** prop (similar to `UFormField`) and will automatically inherit their state from their parent form.

```diff
<template>
  <UForm :state="state" :schema="schema" @submit="onSubmit">
    <UFormField label="Customer" name="customer">
      <UInput v-model="state.customer" placeholder="Wonka Industries" />
    </UFormField>

    <div v-for="(item, index) in state.items" :key="index">
      <UForm
-       :state="item"
+       :name="`items.${index}`"
        :schema="itemSchema"
+       nested
      >
        <UFormField :label="!index ? 'Description' : undefined" name="description">
          <UInput v-model="item.description" />
        </UFormField>
        <UFormField :label="!index ? 'Price' : undefined" name="price">
          <UInput v-model="item.price" type="number" />
        </UFormField>
      </UForm>
    </div>
  </UForm>
</template>
```

### Removed deprecated utilities

Some **Nuxt Content utilities** that were previously available in Nuxt UI Pro have been **removed** in v4:

- `findPageBreadcrumb`
- `findPageHeadline`

These are now fully provided by Nuxt Content. Make sure to update your imports and usage accordingly.

```diff
- import { findPageHeadline } from '@nuxt/ui-pro/utils/content'
+ import { findPageHeadline } from '@nuxt/content/utils'

- import { findPageBreadcrumb } from '@nuxt/ui-pro/utils/content'
+ import { findPageBreadcrumb } from '@nuxt/content/utils'
```

### AI SDK v5 migration (optional)

This section only applies if you're using the AI SDK and chat components (`ChatMessage`, `ChatMessages`, `ChatPrompt`, `ChatPromptSubmit`, `ChatPalette`). If you're not using AI features, you can skip this section.

1. Update `@ai-sdk/vue` and `ai` dependencies in your `package.json`:

```diff
{
  "dependencies": {
-   "@ai-sdk/vue": "^1.2.x",
+   "@ai-sdk/vue": "^2.0.x",
-   "ai": "^4.3.x"
+   "ai": "^5.0.x"
  }
}
```

2. `useChat` composable has been replaced with the new `Chat` class:

```diff
<script setup lang="ts">
- import { useChat } from '@ai-sdk/vue'
+ import { Chat } from '@ai-sdk/vue'
+ import type { UIMessage } from 'ai'

- const { messages, input, handleSubmit, status, error, reload, setMessages } = useChat()
+ const messages: UIMessage[] = []
+ const input = ref('')
+
+ const chat = new Chat({
+   messages
+ })
+
+ function handleSubmit() {
+   chat.sendMessage({ text: input.value })
+   input.value = ''
+ }
</script>
```

3. Messages now use `parts` instead of `content`:

```diff
// When manually creating messages
- setMessages([{
+ messages.push({
  id: '1',
  role: 'user',
- content: 'Hello world'
+ parts: [{ type: 'text', text: 'Hello world' }]
- }])
+ })

// In templates
<template>
- <UChatMessage :content="message.content" />
+ <UChatMessage :parts="message.parts" />
</template>
```

4. Some methods have been renamed:

```diff
// Regenerate the last message
- reload()
+ chat.regenerate()

// Access chat state
- :messages="messages"
- :status="status"
+ :messages="chat.messages"
+ :status="chat.status"
```

5. Parts-based rendering with AI SDK helpers and `isReasoningStreaming` utility from `@nuxt/ui/utils/ai`:

```vue
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart } from 'ai'
import { isReasoningStreaming } from '@nuxt/ui/utils/ai'
</script>

<template>
  <UChatMessages :messages="chat.messages" :status="chat.status">
    <template #content="{ message }">
      <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
        <UChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isReasoningStreaming(message, index, chat)"
        />

        <MDC
          v-else-if="isTextUIPart(part)"
          :value="part.text"
          :cache-key="`${message.id}-${index}`"
          class="*:first:mt-0 *:last:mb-0"
        />
      </template>
    </template>
  </UChatMessages>
</template>
```

\> \[!NOTE]
\> See: https\://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
\> For more details on AI SDK v5 changes, review the official AI SDK v5 migration guide.

\> \[!TIP]
\> See: https\://github.com/nuxt/ui/pull/4698
\> View all changes from AI SDK v4 to v5 in the upgrade PR for a detailed migration reference.


# Migration to v3

Nuxt UI v3 is a new major version rebuilt from the ground up, introducing a modern architecture with significant performance improvements and an enhanced developer experience. This major release includes several breaking changes alongside powerful new features and capabilities:

- **Tailwind CSS v4**: Migration from JavaScript to CSS-based configuration
- **Reka UI**: Replacing Headless UI as the underlying component library
- **Tailwind Variants**: New styling API for component variants

This guide provides step by step instructions to migrate your application from v2 to v3.

## Migrate your project

Update Tailwind CSSTailwind CSS v4 introduces significant changes to its configuration approach. The official Tailwind upgrade tool will help automate most of the migration process.
\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/upgrade-guide#changes-from-v3
\> For a detailed walkthrough of all changes, refer to the official Tailwind CSS v4 upgrade guide.
Create a main.css file and import it in your nuxt.config.ts file:
\`\`\`css
@import "tailwindcss";
\`\`\`
\`\`\`ts
export default defineNuxtConfig({
css: \['\~/assets/css/main.css']
})
\`\`\`
Run the Tailwind CSS upgrade tool:
\`\`\`bash
npx @tailwindcss/upgrade
\`\`\`
Update Nuxt UIInstall the latest version of the package:
\`\`\`bash
pnpm add @nuxt/ui
\`\`\`
\`\`\`bash
yarn add @nuxt/ui
\`\`\`
\`\`\`bash
npm install @nuxt/ui
\`\`\`
\`\`\`bash
bun add @nuxt/ui
\`\`\`
Import it in your CSS:
\`\`\`css
@import "tailwindcss";
@import "@nuxt/ui";
\`\`\`
Wrap your app with the App component:
\`\`\`vue
\<template>
\<UApp>
\<NuxtPage />
\</UApp>
\</template>
\`\`\`

## Changes from v2

Now that you have updated your project, you can start migrating your code. Here's a comprehensive list of all the breaking changes in Nuxt UI v3.

### Updated design system

In Nuxt UI v2, we had a mix between a design system with `primary`, `gray`, `error` aliases and all the colors from Tailwind CSS. We've replaced it with a proper [design system](https://ui.nuxt.com/docs/getting-started/theme/design-system) with 7 color aliases:

| Color                          | Default  | Description                                                 |
| ------------------------------ | -------- | ----------------------------------------------------------- |
| `primary`{color="primary"}     | `green`  | Main brand color, used as the default color for components. |
| `secondary`{color="secondary"} | `blue`   | Secondary color to complement the primary color.            |
| `success`{color="success"}     | `green`  | Used for success states.                                    |
| `info`{color="info"}           | `blue`   | Used for informational states.                              |
| `warning`{color="warning"}     | `yellow` | Used for warning states.                                    |
| `error`{color="error"}         | `red`    | Used for form error validation states.                      |
| `neutral`                      | `slate`  | Neutral color for backgrounds, text, etc.                   |

This change introduces several breaking changes that you need to be aware of:

- The `gray` color has been renamed to `neutral`

```diff
<template>
- <p class="text-gray-500 dark:text-gray-400" />
+ <p class="text-neutral-500 dark:text-neutral-400" />
</template>
```

\> \[!NOTE]
\> You can also use the new \[design tokens]\(/docs/getting-started/theme/css-variables) to handle light and dark mode:
\> \`\`\`diff
\> \<template>
\> - \<p class="text-gray-500 dark\:text-gray-400" />
\> + \<p class="text-muted" />
\>
\> - \<p class="text-gray-900 dark\:text-white" />
\> + \<p class="text-highlighted" />
\> \</template>
\>
\> \`\`\`

- The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:

```diff
- <UButton color="black" />
+ <UButton color="neutral" />

- <UButton color="gray" />
+ <UButton color="neutral" variant="subtle" />

- <UButton color="white" />
+ <UButton color="neutral" variant="outline" />
```

- You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:

```diff
- <UButton color="red" />
+ <UButton color="error" />
```

\> \[!NOTE]
\> See: /docs/getting-started/theme/design-system#colors
\> Learn how to extend the design system to add new color aliases.

- The color configuration in `app.config.ts` has been moved into a `colors` object:

```diff
export default defineAppConfig({
  ui: {
-   primary: 'green',
-   gray: 'cool'
+   colors: {
+     primary: 'green',
+     neutral: 'slate'
+   }
  }
})
```

### Updated theming system

Nuxt UI components are now styled using the [Tailwind Variants API](https://ui.nuxt.com/docs/getting-started/theme/components), which makes all the overrides you made using the `app.config.ts` and the `ui` prop obsolete.

- Update your [`app.config.ts`](https://ui.nuxt.com/docs/getting-started/theme/components#global-config) to override components with their new theme:

```diff
export default defineAppConfig({
   ui: {
     button: {
-       font: 'font-bold',
-       default: {
-         size: 'md',
-         color: 'primary'
-       }
+       slots: {
+         base: 'font-medium'
+       },
+       defaultVariants: {
+         size: 'md',
+         color: 'primary'
+       }
     }
   }
})
```

- Update your [`ui` props](https://ui.nuxt.com/docs/getting-started/theme/components#ui-prop) to override each component's slots using their new theme:

```diff
<template>
- <UButton :ui="{ font: 'font-bold' }" />
+ <UButton :ui="{ base: 'font-bold' }" />
</template>
```

\> \[!TIP]
\> See: /docs/components/button#theme
\> We can't detail all the changes here but you can check each component's theme in the Theme section.

### Renamed components

We've renamed some Nuxt UI components to align with the Reka UI naming convention:

| v2                     | v3                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `Divider`              | [`Separator`](https://ui.nuxt.com/docs/components/separator)                                            |
| `Dropdown`             | [`DropdownMenu`](https://ui.nuxt.com/docs/components/dropdown-menu)                                     |
| `FormGroup`            | [`FormField`](https://ui.nuxt.com/docs/components/form-field)                                           |
| `Range`                | [`Slider`](https://ui.nuxt.com/docs/components/slider)                                                  |
| `Toggle`               | [`Switch`](https://ui.nuxt.com/docs/components/switch)                                                  |
| `Meter`                | Removed                                                                                                 |
| `Notification`         | [`Toast`](https://ui.nuxt.com/docs/components/toast)                                                    |
| `Radio`                | Removed (use [`RadioGroup`](https://ui.nuxt.com/docs/components/radio-group) instead)                   |
| `VerticalNavigation`   | [`NavigationMenu`](https://ui.nuxt.com/docs/components/navigation-menu) with `orientation="vertical"`   |
| `HorizontalNavigation` | [`NavigationMenu`](https://ui.nuxt.com/docs/components/navigation-menu) with `orientation="horizontal"` |

Here are the Nuxt UI Pro components that have been renamed or removed:

| v1                      | v3                                                                                                                                 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `BlogList`              | [`BlogPosts`](https://ui.nuxt.com/docs/components/blog-posts)                                                                      |
| `ColorModeToggle`       | [`ColorModeSwitch`](https://ui.nuxt.com/docs/components/color-mode-switch)                                                         |
| `DashboardCard`         | Removed (use [`PageCard`](https://ui.nuxt.com/docs/components/page-card) instead)                                                  |
| `DashboardLayout`       | [`DashboardGroup`](https://ui.nuxt.com/docs/components/dashboard-group)                                                            |
| `DashboardModal`        | Removed (use [`Modal`](https://ui.nuxt.com/docs/components/modal) instead)                                                         |
| `DashboardNavbarToggle` | [`DashboardSidebarToggle`](https://ui.nuxt.com/docs/components/dashboard-sidebar-toggle)                                           |
| `DashboardPage`         | Removed                                                                                                                            |
| `DashboardPanelContent` | Removed (use `#body` slot instead)                                                                                                 |
| `DashboardPanelHandle`  | [`DashboardResizeHandle`](https://ui.nuxt.com/docs/components/dashboard-resize-handle)                                             |
| `DashboardSection`      | Removed (use [`PageCard`](https://ui.nuxt.com/docs/components/page-card) instead)                                                  |
| `DashboardSidebarLinks` | Removed (use [`NavigationMenu`](https://ui.nuxt.com/docs/components/navigation-menu) instead)                                      |
| `DashboardSlideover`    | Removed (use [`Slideover`](https://ui.nuxt.com/docs/components/slideover) instead)                                                 |
| `FooterLinks`           | Removed (use [`NavigationMenu`](https://ui.nuxt.com/docs/components/navigation-menu) instead)                                      |
| `HeaderLinks`           | Removed (use [`NavigationMenu`](https://ui.nuxt.com/docs/components/navigation-menu) instead)                                      |
| `LandingCard`           | Removed (use [`PageCard`](https://ui.nuxt.com/docs/components/page-card) instead)                                                  |
| `LandingCTA`            | [`PageCTA`](https://ui.nuxt.com/docs/components/page-cta)                                                                          |
| `LandingFAQ`            | Removed (use [`Accordion`](https://ui.nuxt.com/docs/components/accordion) instead)                                                 |
| `LandingGrid`           | Removed (use [`PageGrid`](https://ui.nuxt.com/docs/components/page-grid) instead)                                                  |
| `LandingHero`           | Removed (use [`PageHero`](https://ui.nuxt.com/docs/components/page-hero) instead)                                                  |
| `LandingLogos`          | [`PageLogos`](https://ui.nuxt.com/docs/components/page-logos)                                                                      |
| `LandingSection`        | [`PageSection`](https://ui.nuxt.com/docs/components/page-section)                                                                  |
| `LandingTestimonial`    | Removed (use [`PageCard`](https://ui.nuxt.com/docs/components/page-card#as-a-testimonial) instead)                                 |
| `NavigationAccordion`   | [`ContentNavigation`](https://ui.nuxt.com/docs/components/content-navigation)                                                      |
| `NavigationLinks`       | [`ContentNavigation`](https://ui.nuxt.com/docs/components/content-navigation)                                                      |
| `NavigationTree`        | [`ContentNavigation`](https://ui.nuxt.com/docs/components/content-navigation)                                                      |
| `PageError`             | [`Error`](https://ui.nuxt.com/docs/components/error)                                                                               |
| `PricingCard`           | [`PricingPlan`](https://ui.nuxt.com/docs/components/pricing-plan)                                                                  |
| `PricingGrid`           | [`PricingPlans`](https://ui.nuxt.com/docs/components/pricing-plans)                                                                |
| `PricingSwitch`         | Removed (use [`Switch`](https://ui.nuxt.com/docs/components/switch) or [`Tabs`](https://ui.nuxt.com/docs/components/tabs) instead) |

### Changed components

In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:

- The `links` and `options` props have been renamed to `items` for consistency:

```diff
<template>
- <USelect :options="countries" />
+ <USelect :items="countries" />

- <UHorizontalNavigation :links="links" />
+ <UNavigationMenu :items="links" />
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Breadcrumb\`, \`HorizontalNavigation\`, \`InputMenu\`, \`RadioGroup\`, \`Select\`, \`SelectMenu\`, \`VerticalNavigation\`.

- The `click` field in different components has been removed in favor of the native Vue `onClick` event:

```diff
<script setup lang="ts">
const items = [{
  label: 'Edit',
-  click: () => {
+  onClick: () => {
    console.log('Edit')
  }
}]
</script>
```

\> \[!NOTE]
\> This change affects the \`Toast\` component as well as all component that have \`items\` links like \`NavigationMenu\`, \`DropdownMenu\`, \`CommandPalette\`, etc.

- The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor of the [App](https://ui.nuxt.com/docs/components/app) component:

```diff [app.vue]
<template>
+  <UApp>
+    <NuxtPage />
+  </UApp>
-  <UModals />
-  <USlideovers />
-  <UNotifications />
</template>
```

- The `v-model:open` directive and `default-open` prop are now used to control visibility:

```diff
<template>
- <UModal v-model="open" />
+ <UModal v-model:open="open" />
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`ContextMenu\`, \`Modal\` and \`Slideover\` and enables controlling visibility for \`InputMenu\`, \`Select\`, \`SelectMenu\` and \`Tooltip\`.

- The default slot is now used for the trigger and the content goes inside the `#content` slot (you don't need to use a `v-model:open` directive with this method):

```diff
<script setup lang="ts">
- const open = ref(false)
</script>

<template>
- <UButton label="Open" @click="open = true" />

- <UModal v-model="open">
+ <UModal>
+   <UButton label="Open" />

+   <template #content>
      <div class="p-4">
        <Placeholder class="h-48" />
      </div>
+   </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Modal\`, \`Popover\`, \`Slideover\`, \`Tooltip\`.

- A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like:

```diff
<template>
- <UModal>
+ <UModal title="Title" description="Description">
-   <div class="p-4">
+   <template #body>
      <Placeholder class="h-48" />
+   </template>
-   </div>
  </UModal>
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Modal\`, \`Slideover\`.

- The `prevent-close` prop has been removed in favor of the `dismissible` prop:

```diff
<template>
- <UModal prevent-close />
+ <UModal :dismissible="false" />
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Modal\`, \`Slideover\`.

- The `Pagination` component `v-model` directive has been renamed to `v-model:page`:

```diff
<template>
- <UPagination v-model="page" />
+ <UPagination v-model:page="page" />
</template>
```

- The `change` event now emits the native `change` event, not the new value, which is now emitted in the `update:modelValue` event:

```diff
<template>
- <USelectMenu v-model="country" :items="countries" @change="console.log(newVal)" />
+ <USelectMenu v-model="country" :items="countries" @update:modelValue="console.log(newVal)" />
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Select\`, \`SelectMenu\`, \`RadioGroup\`.

- The `SelectMenu` component `searchable` prop has been renamed to `search-input` and now defaults to `true`. To preserve v2 behavior (no search input):

```diff
<template>
- <USelectMenu :items="items" />
+ <USelectMenu :search-input="false" :items="items" />
</template>
```

- The `Accordion` component has been redesigned. The `multiple` prop has been replaced by the `type` prop (defaults to `single`):

```diff
<template>
- <UAccordion multiple :items="items" />
+ <UAccordion type="multiple" :items="items" />
</template>
```

- The `Accordion` component `default-open` prop and `defaultOpen` **item** property have been removed. State is now controlled using `default-value` (uncontrolled) or `v-model` (controlled):

```diff
<template>
- <UAccordion default-open multiple :items="items" />
+ <UAccordion
+   type="multiple"
+   :default-value="['0', '1']"
+   :items="items"
+ />
</template>
```

- The `Accordion` component `#item` slot has been removed in favor of `#content` and `#body`:

```diff
<template>
- <template #item="{ item }">
-   {{ item.content }}
- </template>

+ <template #content="{ item }">
+   {{ item.content }}
+ </template>
</template>
```

\> \[!NOTE]
\> The default slot now only customizes the trigger, with additional slots for finer control (\`#leading\`, \`#trailing\`, \`#body\`).

- The `Accordion` component `unmount` prop has been renamed to `unmount-on-hide` and now defaults to `true`. To preserve v2 behavior (keep content mounted), use `:unmount-on-hide="false"`:

```diff
<template>
- <UAccordion :items="items" />
+ <UAccordion :unmount-on-hide="false" :items="items" />
</template>
```

- The `Table` component now uses [TanStack Table](https://tanstack.com/table/latest){rel="&#x22;nofollow&#x22;"} under the hood. The `rows` prop has been renamed to `data`:

```diff
<template>
- <UTable :rows="rows" />
+ <UTable :data="data" />
</template>
```

- The `Table` component columns definition is now explicit and semantic:

```diff
<script setup lang="ts">
const columns = [{
-  label: 'Status',
-  key: 'status'
+  header: 'Status',
+  accessorKey: 'status'
}]
</script>
```

- The `Table` component row cell slot names have been changed from `<column-accessorKey>-data` to `<column-accessorKey>-cell`:

```diff
<template>
- <template #column-data="{ row }">
+ <template #column-cell="{ row }">
</template>
```

- The `Tabs` component `#item` slot has been removed in favor of `#content`:

```diff
<template>
- <template #item="{ item }">
+ <template #content="{ item }">
</template>
```

- The `Tabs` component `default-index` prop has been removed in favor of `default-value`:

```diff
<template>
- <UTabs :default-index="0" :items="tabs" />
+ <UTabs :default-value="0" :items="tabs" />
</template>
```

- The `Tabs` component `unmount` prop has been renamed to `unmount-on-hide` and now defaults to `true`. To preserve v2 behavior where content stayed mounted:

```diff
<template>
- <UTabs :items="tabs" />
+ <UTabs :unmount-on-hide="false" :items="tabs" />
</template>
```

- The `Alert` component `close-button` prop has been replaced by the `close` prop:

```diff
<template>
- <UAlert :close-button="{ icon: 'i-lucide-x', variant: 'link' }" />
+ <UAlert :close="{ icon: 'i-lucide-x', variant: 'link' }" />
</template>
```

- The `Alert` component `close` event has been replaced by the `update:open` event:

```diff
<template>
- <UAlert @close="isOpen = false" />
+ <UAlert @update:open="isOpen = false" />
</template>
```

- The `Alert` component `#icon` and `#avatar` slots have been replaced by a single `#leading` slot:

```diff
<template>
- <UAlert>
-   <template #icon>
-     <UIcon name="i-lucide-terminal" />
-   </template>
- </UAlert>

+ <UAlert>
+   <template #leading>
+     <UIcon name="i-lucide-terminal" />
+   </template>
+ </UAlert>
</template>
```

- The `Form` component now always validates on submit. The `validate-on` prop only controls which input events trigger validation. Pass an empty array to validate only on submit:

```diff
<template>
- <UForm :validate-on="['submit']" />
+ <UForm :validate-on="[]" />
</template>
```

- Form components now use `inline-flex` instead of `block` layout, which means they no longer expand to full width by default. Add `w-full` manually with the `class` prop or configure it globally in your `app.config.ts`:

```ts [app/app.config.ts]
export default defineAppConfig({
  ui: {
    input: { slots: { root: 'w-full' } },
    inputMenu: { slots: { root: 'w-full' } },
    textarea: { slots: { root: 'w-full' } },
    select: { slots: { base: 'w-full' } },
    selectMenu: { slots: { base: 'w-full' } }
  }
})
```

\> \[!NOTE]
\> This change affects the following components: \`Input\`, \`InputMenu\`, \`Textarea\`, \`Select\`, \`SelectMenu\`.

- The `popper` prop has been replaced by `content` for positioning:

```diff
<template>
- <UTooltip :popper="{ placement: 'top' }" />
+ <UTooltip :content="{ side: 'top' }" />

- <USelectMenu :popper="{ placement: 'bottom-start' }" />
+ <USelectMenu :content="{ side: 'bottom', align: 'start' }" />
</template>
```

\> \[!NOTE]
\> This change affects the following components: \`Tooltip\`, \`Popover\`, \`DropdownMenu\`, \`ContextMenu\`, \`SelectMenu\`, \`InputMenu\`.

- The `Tooltip` component `shortcuts` prop has been renamed to `kbds` and `prevent` to `disabled`:

```diff
<template>
- <UTooltip text="Open" :shortcuts="['⌘', 'O']" />
+ <UTooltip text="Open" :kbds="['meta', 'O']" />
</template>
```

- The `Popover` component `#panel` slot has been renamed to `#content`:

```diff
<template>
  <UPopover>
    <UButton label="Open" />

-   <template #panel>
+   <template #content>
      <div class="p-4">Content</div>
    </template>
  </UPopover>
</template>
```

- The `ContextMenu` component has been completely redesigned. It now uses items and has a proper trigger/content structure:

```diff
<template>
- <UContextMenu v-model="isOpen" :virtual-element="virtualElement" />
+ <UContextMenu :items="items">
+   <div>Right-click me</div>
+ </UContextMenu>
</template>
```

- The `Progress` component `value` prop has been replaced by `model-value` and `indicator` by `status`:

```diff
<template>
- <UProgress :value="50" indicator />
+ <UProgress :model-value="50" status />
</template>
```

- The `Carousel` component `indicators` prop has been renamed to `dots`:

```diff
<template>
- <UCarousel :items="items" indicators />
+ <UCarousel :items="items" dots />
</template>
```

\> \[!NOTE]
\> The \`Carousel\` component now uses \[Embla Carousel]\(https\://www\.embla-carousel.com/) under the hood.

- The `help` prop/property has been renamed to `description`:

```diff
<template>
- <UCheckbox label="Remember me" help="Save my login details" />
+ <UCheckbox label="Remember me" description="Save my login details" />
</template>

<script setup lang="ts">
const items = [{
  label: 'Option 1',
- help: 'Description for option 1'
+ description: 'Description for option 1'
}]
</script>
```

\> \[!NOTE]
\> This change affects the following components: \`Checkbox\`, \`RadioGroup\`.

- The `Breadcrumb` component `divider` prop has been renamed to `separator-icon` and `#divider` slot to `#separator`:

```diff
<template>
- <UBreadcrumb :links="links" divider="i-lucide-arrow-right" />
+ <UBreadcrumb :items="items" separator-icon="i-lucide-arrow-right" />
</template>
```

- The `Avatar` component chip props (`chip-color`, `chip-position`, `chip-text`) have been consolidated into a single `chip` prop:

```diff
<template>
- <UAvatar src="..." chip-color="green" chip-position="top-right" chip-text="" />
+ <UAvatar src="..." :chip="{ color: 'success', position: 'top-right' }" />
</template>
```

- The `Button` component `padded` and `truncate` props have been removed. Use `square` instead of `:padded="false"`:

```diff
<template>
- <UButton :padded="false" />
+ <UButton square />
</template>
```

- The `Chip` component `show` prop is now a model (`v-model:show`):

```diff
<template>
- <UChip :show="isVisible" />
+ <UChip v-model:show="isVisible" />
</template>
```

- The `CommandPalette` component `groups` prop structure has changed. Each group now has an `items` array and uses `onSelect` instead of `click`:

```diff
<script setup lang="ts">
const groups = [{
  id: 'actions',
  label: 'Actions',
- commands: [{ id: 'new', label: 'New file' }]
+ items: [{ id: 'new', label: 'New file' }]
}]
</script>
```

### Changed composables

- The `useToast()` composable `timeout` prop has been renamed to `duration`:

```diff
<script setup lang="ts">
const toast = useToast()

- toast.add({ title: 'Invitation sent', timeout: 0 })
+ toast.add({ title: 'Invitation sent', duration: 0 })
</script>
```

- The `useModal` and `useSlideover` composables have been removed in favor of a more generic `useOverlay` composable:

Some important differences:

- The `useOverlay` composable is now used to create overlay instances
- Overlays that are opened, can be awaited for their result
- Overlays can no longer be close using `modal.close()` or `slideover.close()`, rather, they close automatically: either when a `close` event is fired explicitly from the opened component OR when the overlay closes itself (clicking on backdrop, pressing the ESC key, etc)
- To capture the return value in the parent component you must explicitly emit a `close` event with the desired value

```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'

- const modal = useModal()
+ const overlay = useOverlay()

- modal.open(ModalExampleComponent)
+ const modal = overlay.create(ModalExampleComponent)
</script>
```

Props are now passed through a props attribute:

```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'

- const modal = useModal()
+ const overlay = useOverlay()

const count = ref(0)

- modal.open(ModalExampleComponent, {
-   count: count.value
- })
+ const modal = overlay.create(ModalExampleComponent, {
+   props: {
+     count: count.value
+   }
+ })
</script>
```

Closing a modal is now done through the `close` event. The `modal.open` method now returns an instance that can be used to await for the result of the modal whenever the modal is closed:

```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'

- const modal = useModal()
+ const overlay = useOverlay()

+ const modal = overlay.create(ModalExampleComponent)

- function openModal() {
-   modal.open(ModalExampleComponent, {
-     onSuccess() {
-       toast.add({ title: 'Success!' })
-     }
-   })
- }
+ async function openModal() {
+   const instance = modal.open(ModalExampleComponent, {
+     count: count.value
+   })
+
+   const result = await instance.result
+
+   if (result) {
+     toast.add({ title: 'Success!' })
+   }
+ }
</script>
```

### Changed form validation

- The error object property for targeting form fields has been renamed from `path` to `name`:

```diff
<script setup lang="ts">
function validate(state: any): FormError[] {
  const errors = []
  if (!state.email) {
    errors.push({
-     path: 'email',
+     name: 'email',
      message: 'Required'
    })
  }
  if (!state.password) {
    errors.push({
-     path: 'password',
+     name: 'password',
      message: 'Required'
    })
  }
  return errors
}
</script>
```

---

\> \[!WARNING]
\> This page is a work in progress, we'll improve it regularly.


# Contribution

Nuxt UI thrives thanks to its incredible community ❤️. We welcome all contributions through bug reports, pull requests, and feedback to help make this library even better.

\> \[!CAUTION]
\> Before reporting a bug or requesting a feature, make sure that you have read through our \[documentation]\(https\://ui.nuxt.com/) and existing \[issues]\(https\://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc).

## AI assistance `New`

We provide a [skill](https://github.com/nuxt/ui/tree/v4/.claude/skills/contributing){rel="&#x22;nofollow&#x22;"} for AI assistants to help you contribute to Nuxt UI. It will automatically guide you through component structure, theming patterns, testing conventions, and documentation guidelines when working in this repository.

## Project structure

Here's an overview of the key directories and files in the Nuxt UI project structure:

### Documentation

The documentation lives in the `docs` folder as a Nuxt app using `@nuxt/content` to generate pages from Markdown files. See the [Nuxt Content documentation](https://content.nuxt.com/docs/getting-started){rel="&#x22;nofollow&#x22;"} for details on how it works. Here's a breakdown of its structure:

```bash
├── app/
│   ├── assets/
│   ├── components/
│   │   └── content/
│   │       └── examples   # Components used in documentation as examples
│   ├── composables/
│   └── ...
├── content/
│   ├── 1.getting-started
│   ├── 2.composables
│   └── 3.components       # Components documentation
```

### Module

The module code resides in the `src` folder. Here's a breakdown of its structure:

```bash
├── plugins/
├── runtime/
│   ├── components/        # Where all the components are located
│   │   ├── Accordion.vue
│   │   ├── Alert.vue
│   │   └── ...
│   ├── composables/
│   ├── locale/
│   ├── plugins/
│   ├── types/
│   ├── utils/
│   └── vue/
│       ├── components/
│       └── plugins/
├── theme/                 # This where the theme for each component is located
│   ├── accordion.ts       # Theme for Accordion component
│   ├── alert.ts
│   └── ...
└── module.ts
```

## CLI

To make development easier, we've created a CLI that you can use to generate components and locales. You can access it using the `nuxt-ui make` command.

First, you need to link the CLI to your global environment:

```sh
npm link
```

### Components

You can create new components using the following command:

```sh
nuxt-ui make component <name> [options]
```

Available options:

- `--primitive` Create a primitive component
- `--prose` Create a prose component
- `--content` Create a content component
- `--template` Only generate specific template (available templates: `playground`, `docs`, `test`, `theme`, `component`)

Example:

```sh
# Create a basic component
nuxt-ui make component my-component

# Create a prose component
nuxt-ui make component heading --prose

# Create a content component
nuxt-ui make component block --content

# Generate only documentation template
nuxt-ui make component my-component --template=docs
```

\> \[!NOTE]
\> When creating a new component, the CLI will automatically generate all the necessary files like the component itself, theme, tests, and documentation.

### Locales

You can create new locales using the following command:

```sh
nuxt-ui make locale --code <code> --name <name>
```

\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt#supported-languages
\> Learn more about i18n in the documentation.

## Submit a Pull Request (PR)

Before you start, check if there's an existing issue describing the problem or feature request you're working on. If there is, please leave a comment on the issue to let us know you're working on it.

If there isn't, open a new issue to discuss the problem or feature.

### Local development

To begin local development, follow these steps:

Clone the nuxt/ui repository to your local machine
\`\`\`sh
git clone -b v4 https\://github.com/nuxt/ui.git
\`\`\`
Enable Corepack
\`\`\`sh
corepack enable
\`\`\`
Install dependencies
\`\`\`sh
pnpm install
\`\`\`
Generate type stubs
\`\`\`sh
pnpm run dev\:prepare
\`\`\`
Start developmentTo work on the documentation located in the docs folder, run:
\`\`\`sh
pnpm run docs
\`\`\`
To test the Nuxt components using the playground, run:
\`\`\`sh
pnpm run dev
\`\`\`
To test the Vue components using the playground, run:
\`\`\`sh
pnpm run dev\:vue
\`\`\`

\> \[!NOTE]
\> See: #cli
\> If you're working on implementing a new component, check the CLI section to kickstart the process.

### IDE Setup

We recommend using VSCode alongside the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint){rel="&#x22;nofollow&#x22;"}. You can enable auto-fix and formatting when saving your code. Here's how:

```json [.vscode/settings.json]
{
  "editor.codeActionsOnSave": {
    "source.fixAll": "never",
    "source.fixAll.eslint": "explicit"
  },
  "prettier.enable": false
}
```

\> \[!WARNING]
\> Since ESLint is already configured to format the code, there's no need for duplicating functionality with Prettier. If you have it installed in your editor, we recommend disabling it to avoid conflicts.

### Linting

You can use the `lint` command to check for linting errors:

```sh
pnpm run lint # check for linting errors
pnpm run lint:fix # fix linting errors
```

### Type checking

We use TypeScript for type checking. You can use the `typecheck` command to check for type errors:

```sh
pnpm run typecheck
```

### Testing

Before submitting a PR, ensure that you run the tests:

```sh
pnpm run test
```

\> \[!TIP]
\> If you have to update the snapshots, press \`u\` after the tests have finished running.

### Commit conventions

We use [Conventional Commits](https://www.conventionalcommits.org/){rel="&#x22;nofollow&#x22;"} for commit messages, which allows a changelog to be auto-generated based on the commits. Please read the [guide](https://www.conventionalcommits.org/en/v1.0.0/#summary){rel="&#x22;nofollow&#x22;"} through if you aren't familiar with it already.

- Use `fix` and `feat` for code changes that affect functionality or logic
- Use `docs` for documentation changes and `chore` for maintenance tasks

### Making a Pull Request

- Follow along the [instructions](https://github.com/nuxt/ui/blob/v4/.github/PULL_REQUEST_TEMPLATE.md?plain=1){rel="&#x22;nofollow&#x22;"} provided when creating a PR
- Ensure your PR's title adheres to the [Conventional Commits](https://www.conventionalcommits.org/){rel="&#x22;nofollow&#x22;"} since it will be used once the code is merged.
- Multiple commits are fine; no need to rebase or force push. We'll use `Squash and Merge` when merging.
- Ensure `lint`, `typecheck` and `tests` work before submitting the PR. Avoid making unrelated changes.

We'll review it promptly. If assigned to a maintainer, they'll review it carefully. Ignore the red text; it's for tracking purposes.

## Thanks

Thank you again for being interested in this project! You are awesome! ❤️


# Design System

## Tailwind CSS

Tailwind CSS uses a CSS-first configuration, letting you define your design tokens with the [`@theme`](https://tailwindcss.com/docs/functions-and-directives#theme-directive){rel="&#x22;nofollow&#x22;"} directive directly in your CSS. This makes your theme portable, maintainable and easy to customize.

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
  /* Your custom design tokens go here */
}
```

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/theme
\> Check the Tailwind CSS documentation for all available theme variable customization options.

### Fonts

Use the `--font-*` theme variables to [customize the font family utilities](https://tailwindcss.com/docs/font-family#customizing-your-theme){rel="&#x22;nofollow&#x22;"} in your project.

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
  --font-sans: 'Public Sans', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
}
```

\*\*Nuxt:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/fonts
\> Fonts defined here are automatically loaded and optimized by the \`@nuxt/fonts\` module.

### Colors

Use the `--color-*` theme variables to [customize your colors](https://tailwindcss.com/docs/colors#customizing-your-colors){rel="&#x22;nofollow&#x22;"} or [override default colors](https://tailwindcss.com/docs/colors#overriding-default-colors){rel="&#x22;nofollow&#x22;"}.

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@theme static {
  /* Override default green color */
  --color-green-50: #EFFDF5;
  --color-green-100: #D9FBE8;
  --color-green-200: #B3F5D1;
  --color-green-300: #75EDAE;
  --color-green-400: #00DC82;
  --color-green-500: #00C16A;
  --color-green-600: #00A155;
  --color-green-700: #007F45;
  --color-green-800: #016538;
  --color-green-900: #0A5331;
  --color-green-950: #052E16;

  /* Define new custom color */
  --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;
}
```

\> \[!WARNING]
\> When adding custom colors, make sure to define all shades from \`50\` to \`950\` for each color.

### Breakpoints

Use the `--breakpoint-*` theme variables to [customize your breakpoints](https://tailwindcss.com/docs/responsive-design#customizing-your-theme){rel="&#x22;nofollow&#x22;"}.

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
  --breakpoint-3xl: 1920px;
  --breakpoint-4xl: 2560px;
  --breakpoint-5xl: 3840px;
}
```

## Colors

Nuxt UI's color system is designed around **semantic naming** rather than specific color values. This approach makes your UI more maintainable and allows for easy theme switching.

### Semantic colors

Nuxt UI provides semantic color aliases that describe the **purpose** of the color. Each alias is defined based on a color from your `@theme` configuration, which can be any color you define in addition to the [default Tailwind CSS palette](https://tailwindcss.com/docs/colors){rel="&#x22;nofollow&#x22;"}.

| Color                          | Default  | Description                                                       |
| ------------------------------ | -------- | ----------------------------------------------------------------- |
| `primary`{color="primary"}     | `green`  | Main CTAs, active navigation, brand elements, important links     |
| `secondary`{color="secondary"} | `blue`   | Secondary buttons, alternative actions, complementary UI elements |
| `success`{color="success"}     | `green`  | Success messages, completed states, positive confirmations        |
| `info`{color="info"}           | `blue`   | Info alerts, tooltips, help text, neutral notifications           |
| `warning`{color="warning"}     | `yellow` | Warning messages, pending states, attention-needed items          |
| `error`{color="error"}         | `red`    | Error messages, validation errors, destructive actions            |
| `neutral`                      | `slate`  | Text, borders, backgrounds, disabled states                       |

These semantic colors are available in the `color` prop of Nuxt UI components:

```vue
<template>
  <UDesign System color="primary">
    Save Changes
  </UDesign System>
</template>
```

\> \[!NOTE]
\> Try the theme picker in the header to instantly see how different color schemes affect the entire UI!

### Runtime configuration

\*\*Nuxt:\*\*
You can configure these colors at runtime in your app.config.ts file under the ui.colors key, allowing for dynamic theme customization without restarting the server:
\`\`\`ts
export default defineAppConfig({
ui: {
colors: {
primary: 'blue',
secondary: 'purple',
neutral: 'zinc'
}
}
})
\`\`\`
\*\*Vue:\*\*
You can configure these colors at runtime in your vite.config.ts file under the ui.colors key, allowing for dynamic theme customization:
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
colors: {
primary: 'blue',
secondary: 'purple',
neutral: 'zinc'
}
}
})
]
})
\`\`\`

\> \[!CAUTION]
\> You can only use colors that exist in your theme. Either\:Use \[Tailwind's default colors]\(https\://tailwindcss.com/docs/colors) (like \`blue\`, \`green\`, \`zinc\`)Define custom colors first using the \`@theme\` directive (like \`brand\` in our example above)

### Extend colors

You may want to define extra semantic colors beyond the defaults, such as adding a `tertiary` color:

\*\*Nuxt:\*\*
First, register the new color in your nuxt.config.ts under the ui.theme.colors key:
\`\`\`ts
export default defineNuxtConfig({
ui: {
theme: {
colors: \[
'primary',
'secondary',
'tertiary',
'info',
'success',
'warning',
'error'
]
}
}
})
\`\`\`
Then, assign it in your app.config.ts under the ui.colors key:
\`\`\`ts
export default defineAppConfig({
ui: {
colors: {
primary: 'blue',
secondary: 'purple',
tertiary: 'indigo'
}
}
})
\`\`\`
\*\*Vue:\*\*
Register and assign the new color in your vite.config.ts file:
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
theme: {
colors: \[
'primary',
'secondary',
'tertiary',
'info',
'success',
'warning',
'error'
]
},
ui: {
colors: {
primary: 'blue',
secondary: 'purple',
tertiary: 'indigo'
}
}
})
]
})
\`\`\`

Finally, use this new color in components that support the `color` prop or [as a class](https://ui.nuxt.com/docs/getting-started/theme/css-variables):

```vue
<UButton color="tertiary">
  Special Action
</UButton>
```


# CSS Variables

## Colors

Nuxt UI provides Tailwind CSS utility classes for each [semantic color](https://ui.nuxt.com/docs/getting-started/theme/design-system#semantic-colors) you define, allowing you to use class names like `text-error` or `bg-success`:

```vue
<template>
  <p>
  <span>
  Primary</span>
  <span>
  Secondary</span>
  <span>
  Success</span>
  <span>
  Info</span>
  <span>
  Warning</span>
  <span>
  Error</span></p>
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <span class="text-primary">Primary</span>
    <span class="text-secondary">Secondary</span>
    <span class="text-success">Success</span>
    <span class="text-info">Info</span>
    <span class="text-warning">Warning</span>
    <span class="text-error">Error</span>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-primary</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Primary</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-secondary</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Secondary</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-success</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Success</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-info</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Info</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-warning</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Warning</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-error</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Error</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

Each utility class uses a CSS variable to set its color for light and dark modes:

\`\`\`css
\:root {
\--ui-primary: var(--ui-color-primary-500);
\--ui-secondary: var(--ui-color-secondary-500);
\--ui-success: var(--ui-color-success-500);
\--ui-info: var(--ui-color-info-500);
\--ui-warning: var(--ui-color-warning-500);
\--ui-error: var(--ui-color-error-500);
}
\`\`\`
\`\`\`css
.dark {
\--ui-primary: var(--ui-color-primary-400);
\--ui-secondary: var(--ui-color-secondary-400);
\--ui-success: var(--ui-color-success-400);
\--ui-info: var(--ui-color-info-400);
\--ui-warning: var(--ui-color-warning-400);
\--ui-error: var(--ui-color-error-400);
}
\`\`\`

\> \[!TIP]
\> You can adjust which shade each utility class uses for light and dark mode in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-primary: var(--ui-color-primary-700);
\> }
\>
\> .dark {
\> --ui-primary: var(--ui-color-primary-200);
\> }
\>
\> \`\`\`

\> \[!WARNING]
\> You can't use \`primary: 'black'\` in your \[config]\(/docs/getting-started/theme/design-system#runtime-configuration) because \`black\` doesn't have multiple shades. To use solid black or white as your primary color, set it directly in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-primary: black;
\> }
\>
\> .dark {
\> --ui-primary: white;
\> }
\>
\> \`\`\`

## Text

Nuxt UI provides Tailwind CSS utility classes for text colors, allowing you to use class names like `text-dimmed` or `text-muted`:

```vue
<template>
  <p>
  <span>
  Dimmed</span>
  <span>
  Muted</span>
  <span>
  Toned</span>
  <span>
  Text</span>
  <span>
  Highlighted</span>
  <span>
  Inverted</span></p>
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <span class="text-dimmed">Dimmed</span>
    <span class="text-muted">Muted</span>
    <span class="text-toned">Toned</span>
    <span class="text-default">Text</span>
    <span class="text-highlighted">Highlighted</span>
    <span class="text-inverted bg-inverted">Inverted</span>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-dimmed</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Dimmed</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-muted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Muted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-toned</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Toned</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-default</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Text</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-highlighted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Highlighted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  span</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  text-inverted bg-inverted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Inverted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  span</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

Each utility class uses a CSS variable to set its color for light and dark modes:

\`\`\`css
\:root {
\--ui-text-dimmed: var(--ui-color-neutral-400);
\--ui-text-muted: var(--ui-color-neutral-500);
\--ui-text-toned: var(--ui-color-neutral-600);
\--ui-text: var(--ui-color-neutral-700);
\--ui-text-highlighted: var(--ui-color-neutral-900);
\--ui-text-inverted: white;
}
\`\`\`
\`\`\`css
.dark {
\--ui-text-dimmed: var(--ui-color-neutral-500);
\--ui-text-muted: var(--ui-color-neutral-400);
\--ui-text-toned: var(--ui-color-neutral-300);
\--ui-text: var(--ui-color-neutral-200);
\--ui-text-highlighted: white;
\--ui-text-inverted: var(--ui-color-neutral-900);
}
\`\`\`

\> \[!TIP]
\> You can customize these CSS variables in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-text: var(--ui-color-neutral-900);
\> }
\>
\> .dark {
\> --ui-text: white;
\> }
\>
\> \`\`\`

## Background

Nuxt UI provides Tailwind CSS utility classes for background colors, allowing you to use class names like `bg-default` or `bg-muted`:

```vue
<template>
  <p>
  <span>
  Default</span>
  <span>
  Muted</span>
  <span>
  Elevated</span>
  <span>
  Accented</span>
  <span>
  Inverted</span></p>
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <div class="bg-default">Default</div>
    <div class="bg-muted">Muted</div>
    <div class="bg-elevated">Elevated</div>
    <div class="bg-accented">Accented</div>
    <div class="bg-inverted text-inverted">Inverted</div>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  bg-default</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Default</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  bg-muted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Muted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  bg-elevated</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Elevated</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  bg-accented</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Accented</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  bg-inverted text-inverted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Inverted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

Each utility class uses a CSS variable to set its color for light and dark modes:

\`\`\`css
\:root {
\--ui-bg: white;
\--ui-bg-muted: var(--ui-color-neutral-50);
\--ui-bg-elevated: var(--ui-color-neutral-100);
\--ui-bg-accented: var(--ui-color-neutral-200);
\--ui-bg-inverted: var(--ui-color-neutral-900);
}
\`\`\`
\`\`\`css
.dark {
\--ui-bg: var(--ui-color-neutral-900);
\--ui-bg-muted: var(--ui-color-neutral-800);
\--ui-bg-elevated: var(--ui-color-neutral-800);
\--ui-bg-accented: var(--ui-color-neutral-700);
\--ui-bg-inverted: white;
}
\`\`\`

\> \[!TIP]
\> You can customize these CSS variables in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-bg: var(--ui-color-neutral-50);
\> }
\>
\> .dark {
\> --ui-bg: var(--ui-color-neutral-950);
\> }
\>
\> \`\`\`

## Border

Nuxt UI provides Tailwind CSS utility classes for border colors, allowing you to use class names like `border-default` or `border-muted`:

```vue
<template>
  <p>
  <span>
  Default</span>
  <span>
  Muted</span>
  <span>
  Accented</span>
  <span>
  Inverted</span></p>
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <div class="border border-default">Default</div>
    <div class="border border-muted">Muted</div>
    <div class="border border-accented">Accented</div>
    <div class="border border-inverted">Inverted</div>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  border border-default</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Default</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  border border-muted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Muted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  border border-accented</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Accented</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  border border-inverted</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  Inverted</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

Each utility class uses a CSS variable to set its color for light and dark modes:

\`\`\`css
\:root {
\--ui-border: var(--ui-color-neutral-200);
\--ui-border-muted: var(--ui-color-neutral-200);
\--ui-border-accented: var(--ui-color-neutral-300);
\--ui-border-inverted: var(--ui-color-neutral-900);
}
\`\`\`
\`\`\`css
.dark {
\--ui-border: var(--ui-color-neutral-800);
\--ui-border-muted: var(--ui-color-neutral-700);
\--ui-border-accented: var(--ui-color-neutral-700);
\--ui-border-inverted: white;
}
\`\`\`

\> \[!TIP]
\> You can customize these CSS variables in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-border: var(--ui-color-neutral-100);
\> }
\>
\> .dark {
\> --ui-border: var(--ui-color-neutral-900);
\> }
\>
\> \`\`\`

## Radius

Nuxt UI overrides Tailwind CSS's default `rounded-*` utilities with a unified border radius system, allowing you to use regular [border radius utilities](https://tailwindcss.com/docs/border-radius){rel="&#x22;nofollow&#x22;"} like `rounded-xs` or `rounded-2xl`:

```vue
<template>
  <p>
  <span>
  xs</span>
  <span>
  sm</span>
  <span>
  md</span>
  <span>
  lg</span>
  <span>
  xl</span>
  <span>
  2xl</span>
  <span>
  3xl</span></p>
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <div class="rounded-xs">xs</div>
    <div class="rounded-sm">sm</div>
    <div class="rounded-md">md</div>
    <div class="rounded-lg">lg</div>
    <div class="rounded-xl">xl</div>
    <div class="rounded-2xl">2xl</div>
    <div class="rounded-3xl">3xl</div>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-xs</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  xs</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-sm</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  sm</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-md</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  md</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-lg</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  lg</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-xl</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  xl</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-2xl</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  2xl</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  div</span>
  <span class=spNyl>
   class</span>
  <span class=sMK4o>
  =</span>
  <span class=sMK4o>
  "</span>
  <span class=sfazB>
  rounded-3xl</span>
  <span class=sMK4o>
  "</span>
  <span class=sMK4o>
  ></span>
  <span class=sTEyZ>
  3xl</span>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  div</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

These utility classes are calculated based on a global `--ui-radius` CSS variable, which defines the base radius value applied across all components for a consistent look.

```css
:root {
  --ui-radius: 0.25rem;
}
```

\> \[!TIP]
\> You can customize the base radius value in your \`main.css\` file:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-radius: 0.5rem;
\> }
\>
\> \`\`\`

\> \[!NOTE]
\> Try the theme picker in the header above to change the base radius value.

## Container

Nuxt UI provides a `--ui-container` CSS variable that controls the maximum width of the [Container](https://ui.nuxt.com/docs/components/container) component.

```css
:root {
  --ui-container: 80rem; /* var(--container-7xl) */
}
```

\> \[!TIP]
\> You can customize this value in your \`main.css\` file to adjust container widths consistently throughout your application:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> @theme {
\> --container-8xl: 90rem;
\> }
\>
\> \:root {
\> --ui-container: var(--container-8xl);
\> }
\>
\> \`\`\`

## Header

Nuxt UI provides a `--ui-header-height` CSS variable that controls the height of the [Header](https://ui.nuxt.com/docs/components/header) component.

```css
:root {
  --ui-header-height: 4rem;
}
```

\> \[!TIP]
\> You can customize this value in your \`main.css\` to adjust header height consistently throughout your application:
\> \`\`\`css
\> @import "tailwindcss";
\> @import "@nuxt/ui";
\>
\> \:root {
\> --ui-header-height: --spacing(24);
\> }
\>
\> \`\`\`

## Body

Nuxt UI applies default classes on the `<body>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="html"} element of your app for consistent theming across light and dark modes:

```css
body {
  @apply antialiased text-default bg-default scheme-light dark:scheme-dark;
}
```


# Customize components

## Tailwind Variants

Nuxt UI components are styled using the [Tailwind Variants](https://www.tailwind-variants.org/){rel="&#x22;nofollow&#x22;"} API, which provides a powerful way to create variants and manage component styles.

### Slots

Components can have multiple `slots`, each representing a distinct HTML element or section within the component. These slots allow for flexible content insertion and styling.

Let's take the [Card](https://ui.nuxt.com/docs/components/card) component as an example which has multiple slots:

\`\`\`ts
export default {
slots: {
root: 'bg-default ring ring-default divide-y divide-default rounded-lg',
header: 'p-4 sm\:px-6',
body: 'p-4 sm\:p-6',
footer: 'p-4 sm\:px-6'
}
}
\`\`\`
\`\`\`vue
\<template>
\<div \:class="ui.root({ class: \[props.ui?.root, props.class] })">
\<div \:class="ui.header({ class: props.ui?.header })">
\<slot name="header" />
\</div>
\<div \:class="ui.body({ class: props.ui?.body })">
\<slot />
\</div>
\<div \:class="ui.footer({ class: props.ui?.footer })">
\<slot name="footer" />
\</div>
\</div>
\</template>
\`\`\`

Some components don't have slots, they are just composed of a single root element. In this case, the theme only defines the `base` slot like the [Container](https://ui.nuxt.com/docs/components/container) component for example:

\`\`\`ts
export default {
base: 'max-w-(--ui-container) mx-auto px-4 sm\:px-6 lg\:px-8'
}
\`\`\`
\`\`\`vue
\<template>
\<div \:class="container({ class: props.class })">
\<slot />
\</div>
\</template>
\`\`\`

\> \[!WARNING]
\> Components without slots don't have a \[\`ui\` prop]\(#ui-prop), only the \[\`class\` prop]\(#class-prop) is available to override styles.

### Variants

Components support `variants`, which allow you to dynamically adjust the styles of different `slots` based on component props.

For example, the [Avatar](https://ui.nuxt.com/docs/components/avatar) component uses a `size` variant to control its appearance:

```ts [src/theme/avatar.ts] {6-18}
export default {
  slots: {
    root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated',
    image: 'h-full w-full rounded-[inherit] object-cover'
  },
  variants: {
    size: {
      sm: {
        root: 'size-7 text-sm'
      },
      md: {
        root: 'size-8 text-base'
      },
      lg: {
        root: 'size-9 text-lg'
      }
    }
  },
  defaultVariants: {
    size: 'md'
  }
}
```

This way, the `size` prop will apply the corresponding styles to the `root` slot:

```vue
<template>
  <UCustomize components src="https://github.com/nuxt.png" size="lg" />
</template>
```

### Default Variants

The `defaultVariants` property sets the default value for each variant when no prop is passed.

For example, the [Avatar](https://ui.nuxt.com/docs/components/avatar) component has its default size set to `md`:

```ts [src/theme/avatar.ts] {19-21}
export default {
  slots: {
    root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated',
    image: 'h-full w-full rounded-[inherit] object-cover'
  },
  variants: {
    size: {
      sm: {
        root: 'size-7 text-sm'
      },
      md: {
        root: 'size-8 text-base'
      },
      lg: {
        root: 'size-9 text-lg'
      }
    }
  },
  defaultVariants: {
    size: 'md'
  }
}
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/installation/nuxt#themedefaultvariants
\> You can use the \`theme.defaultVariants\` option in your \`nuxt.config.ts\` to override the default values for \`size\` and \`color\` for all components at once.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/installation/vue#themedefaultvariants
\> You can use the \`theme.defaultVariants\` option in your \`vite.config.ts\` to override the default values for \`size\` and \`color\` for all components at once.

### Compound Variants

Some components use the `compoundVariants` property to apply classes when multiple variant conditions are met at the same time.

For example, the [Button](https://ui.nuxt.com/docs/components/button) component uses the `compoundVariants` property to apply classes for a specific `color` and `variant` combination:

```ts [src/theme/button.ts] {27-31}
import type { ModuleOptions } from '../module'

export default (options: Required<ModuleOptions>) => ({
  slots: {
    base: ['rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75', options.theme.transitions && 'transition-colors']
  },
  variants: {
    color: {
      ...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
      neutral: ''
    },
    variant: {
      solid: '',
      outline: '',
      soft: '',
      subtle: '',
      ghost: '',
      link: ''
    }
  },
  compoundVariants: [
    ...(options.theme.colors || []).map((color: string) => ({
      color,
      variant: 'outline',
      class: `ring ring-inset ring-${color}/50 text-${color} hover:bg-${color}/10 active:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-${color}`
    })),
    {
      color: 'neutral',
      variant: 'outline',
      class: 'ring ring-inset ring-accented text-default bg-default hover:bg-elevated active:bg-elevated disabled:bg-default aria-disabled:bg-default focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
    }
  ],
  defaultVariants: {
    color: 'primary',
    variant: 'solid'
  }
})
```

## Customize theme

You have multiple ways to customize the appearance of Nuxt UI components, you can do it for all components at once or on a per-component basis.

\> \[!NOTE]
\> Tailwind Variants uses \[\`tailwind-merge\`]\(https\://github.com/dcastil/tailwind-merge) under the hood to merge classes so you don't have to worry about conflicting classes.

\> \[!TIP]
\> You can explore the theme for each component in two ways\:Check the \`Theme\` section in the documentation of each individual component.Browse the source code directly in the GitHub repository at \[\`src/theme\`]\(https\://github.com/nuxt/ui/tree/v4/src/theme).

### Global config

\*\*Nuxt:\*\*
You can override the theme of components globally inside your app.config.ts by using the exact same structure as the theme object.
\*\*Vue:\*\*
You can override the theme of components globally inside your vite.config.ts by using the exact same structure as the theme object.

You can customize the [`slots`](https://ui.nuxt.com/#slots), [`variants`](https://ui.nuxt.com/#variants), [`compoundVariants`](https://ui.nuxt.com/#compound-variants) and [`defaultVariants`](https://ui.nuxt.com/#default-variants) of a component to change the default theme of a component:

\*\*Nuxt:\*\*
\`\`\`ts
export default defineAppConfig({
ui: {
button: {
slots: {
base: 'font-bold'
},
variants: {
size: {
md: {
leadingIcon: 'size-4'
}
}
},
compoundVariants: \[{
color: 'neutral',
variant: 'outline',
class: 'ring-default hover\:bg-accented'
}],
defaultVariants: {
color: 'neutral',
variant: 'outline'
}
}
}
})
\`\`\`
\*\*Vue:\*\*
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
button: {
slots: {
base: 'font-bold'
},
variants: {
size: {
md: {
leadingIcon: 'size-4'
}
}
},
compoundVariants: \[{
color: 'neutral',
variant: 'outline',
class: 'ring-default hover\:bg-accented'
}],
defaultVariants: {
color: 'neutral',
variant: 'outline'
}
}
}
})
]
})
\`\`\`

\> \[!NOTE]
\> In this example, \`font-bold\` overrides \`font-medium\` on all buttons, \`size-4\` overrides \`size-5\` class on the leading icon when \`size="md"\` and \`ring-default hover\:bg-accented\` overrides \`ring-accented hover\:bg-elevated\` when \`color="neutral"\` and \`variant="outline"\`. The buttons now defaults to \`color="neutral"\` and \`variant="outline"\`.

### `ui` prop

You can also override a component's **slots** using the `ui` prop. This takes priority over both global config and resolved `variants`.

```vue
<template>
  <UCustomize components trailing-icon="i-lucide-chevron-right" size="md" color="neutral" variant="outline">
    Button
  </UCustomize components>
</template>
```

\> \[!NOTE]
\> In this example, the \`trailingIcon\` slot is overwritten with \`size-3\` even though the \`md\` size variant would apply a \`size-5\` class to it.

### `class` prop

The `class` prop allows you to override the classes of the `root` or `base` slot. This takes priority over both global config and resolved `variants`.

```vue
<template>
  <UCustomize components class="font-bold rounded-full">
    Button
  </UCustomize components>
</template>
```

\> \[!NOTE]
\> In this example, the \`font-bold\` class will override the default \`font-medium\` class on this button.


# Icons

\> \[!NOTE]
\> See: /docs/getting-started/integrations/icons/vue
\> Looking for the Vue version?

## Usage

Nuxt UI automatically registers the [`@nuxt/icon`](https://github.com/nuxt/icon){rel="&#x22;nofollow&#x22;"} module for you, so there's no additional setup required.

### Icon component

You can use the [Icon](https://ui.nuxt.com/docs/components/icon) component with a `name` prop to display an icon:

```vue
<template>
  <UIcons name="i-lucide-lightbulb" class="size-5" />
</template>
```

\> \[!NOTE]
\> You can use any name from the \[https\://icones.js.org]\(https\://icones.js.org) collection.

### Component props

Some components also have an `icon` prop to display an icon, like the [Button](https://ui.nuxt.com/docs/components/button) for example:

```vue
<template>
  <UIcons icon="i-lucide-sun" variant="subtle">
    Button
  </UIcons>
</template>
```

## Collections

### Iconify dataset

It's highly recommended to install the icon data locally with:

\`\`\`bash
pnpm i @iconify-json/{collection\_name}
\`\`\`
\`\`\`bash
yarn add @iconify-json/{collection\_name}
\`\`\`
\`\`\`bash
npm install @iconify-json/{collection\_name}
\`\`\`

For example, to use the `i-uil-github` icon, install its collection with `@iconify-json/uil`. This way the icons can be served locally or from your serverless functions, which is faster and more reliable on both SSR and client-side.

\> \[!NOTE]
\> See: https\://github.com/nuxt/icon?tab=readme-ov-file#iconify-dataset
\> Read more about this in the \`@nuxt/icon\` documentation.

### Custom local collections

You can use local SVG files to create a custom Iconify collection.

For example, place your icons' SVG files under a folder of your choice, for example, `./app/assets/icons`:

```bash
assets/icons
├── add.svg
└── remove.svg
```

In your `nuxt.config.ts`, add an item in `icon.customCollections`:

```ts
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  icon: {
    customCollections: [{
      prefix: 'custom',
      dir: './app/assets/icons'
    }]
  }
})
```

Then you can use the icons like this:

```vue
<template>
  <UIcon name="i-custom-add" />
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/icon?tab=readme-ov-file#custom-local-collections
\> Read more about this in the \`@nuxt/icon\` documentation.

## Theme

You can change the default icons used by components in your `app.config.ts`:

\*See the interactive theme picker on the documentation website.\*


# Icons

\> \[!NOTE]
\> See: /docs/getting-started/integrations/icons/nuxt
\> Looking for the Nuxt version?

## Usage

### Icon component

You can use the [Icon](https://ui.nuxt.com/docs/components/icon) component with a `name` prop to display an icon:

```vue
<template>
  <UIcons name="i-lucide-lightbulb" class="size-5" />
</template>
```

\> \[!NOTE]
\> You can use any name from the \[https\://icones.js.org]\(https\://icones.js.org) collection.

\> \[!WARNING]
\> When using collections with a dash (\`-\`), you need to separate the icon name from the collection name with a colon (\`:\`) as \`@iconify/vue\` does not handle this case like \`@nuxt/icon\`. For example, instead of \`i-simple-icons-github\` you need to write \`i-simple-icons\:github\` or \`simple-icons\:github\`.Learn more about the \[Iconify naming convention]\(https\://iconify.design/docs/icon-components/vue/#icon).

### Component props

Some components also have an `icon` prop to display an icon, like the [Button](https://ui.nuxt.com/docs/components/button) for example:

```vue
<template>
  <UIcons icon="i-lucide-sun" variant="subtle">
    Button
  </UIcons>
</template>
```

## Theme

You can change the default icons used by Nuxt UI components in your `vite.config.ts`:

\*See the interactive theme picker on the documentation website.\*


# Fonts

## Usage

Nuxt UI automatically registers the [`@nuxt/fonts`](https://github.com/nuxt/fonts){rel="&#x22;nofollow&#x22;"} module for you, so there's no additional setup required.

### Declaration

To use a font in your Nuxt UI application, you can simply declare it in your CSS. It will be automatically loaded and optimized for you.

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
  --font-sans: 'Public Sans', sans-serif;
}
```

### Configuration

You can disable the `@nuxt/fonts` module with the `ui.fonts` option in your `nuxt.config.ts`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  ui: {
    fonts: false
  }
})
```


# Color Mode

\> \[!NOTE]
\> See: /docs/getting-started/integrations/color-mode/vue
\> Looking for the Vue version?

## Usage

Nuxt UI automatically registers the [`@nuxtjs/color-mode`](https://github.com/nuxt-modules/color-mode){rel="&#x22;nofollow&#x22;"} module for you, so there's no additional setup required.

### Components

You can use the built-in [ColorModeAvatar](https://ui.nuxt.com/docs/components/color-mode-avatar) or [ColorModeImage](https://ui.nuxt.com/docs/components/color-mode-image) components to display different images for light and dark mode and the [ColorModeButton](https://ui.nuxt.com/docs/components/color-mode-button), [ColorModeSwitch](https://ui.nuxt.com/docs/components/color-mode-switch) or [ColorModeSelect](https://ui.nuxt.com/docs/components/color-mode-select) components to switch between light and dark modes.

You can also use the [useColorMode](https://color-mode.nuxtjs.org/#usage){rel="&#x22;nofollow&#x22;"} composable to build your own custom component:

```vue [ColorModeButton.vue]
<script setup lang="ts">
const colorMode = useColorMode()

const isDark = computed({
  get() {
    return colorMode.value === 'dark'
  },
  set(_isDark) {
    colorMode.preference = _isDark ? 'dark' : 'light'
  }
})
</script>

<template>
  <ClientOnly v-if="!colorMode?.forced">
    <UButton
      :icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
      color="neutral"
      variant="ghost"
      :aria-label="`Switch to ${isDark ? 'light' : 'dark'} mode`"
      @click="isDark = !isDark"
    />

    <template #fallback>
      <div class="size-8" />
    </template>
  </ClientOnly>
</template>
```

### Configuration

You can disable the `@nuxtjs/color-mode` module with the `ui.colorMode` option in your `nuxt.config.ts`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  ui: {
    colorMode: false
  }
})
```


# Color Mode

\> \[!NOTE]
\> See: /docs/getting-started/integrations/color-mode/nuxt
\> Looking for the Nuxt version?

## Usage

Nuxt UI automatically registers the [useDark](https://vueuse.org/core/useDark){rel="&#x22;nofollow&#x22;"} composable as a Vue plugin, so there's no additional setup required.

### Components

You can use the built-in [ColorModeAvatar](https://ui.nuxt.com/docs/components/color-mode-avatar) or [ColorModeImage](https://ui.nuxt.com/docs/components/color-mode-image) components to display different images for light and dark mode and the [ColorModeButton](https://ui.nuxt.com/docs/components/color-mode-button), [ColorModeSwitch](https://ui.nuxt.com/docs/components/color-mode-switch) or [ColorModeSelect](https://ui.nuxt.com/docs/components/color-mode-select) components to switch between light and dark modes.

You can also use the [useColorMode](https://vueuse.org/core/useColorMode){rel="&#x22;nofollow&#x22;"} composable to build your own custom component:

```vue [ColorModeButton.vue]
<script setup lang="ts">
import { useColorMode } from '@vueuse/core'

const colorMode = useColorMode()

const isDark = computed({
  get() {
    return colorMode.value === 'dark'
  },
  set(_isDark: boolean) {
    colorMode.preference = _isDark ? 'dark' : 'light'
  }
})
</script>

<template>
  <UButton
    :icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
    :color="color"
    :variant="variant"
    :aria-label="`Switch to ${isDark ? 'light' : 'dark'} mode`"
    @click="isDark = !isDark"
  />
</template>
```

### Configuration

You can disable this plugin with the `colorMode` option in your `vite.config.ts`:

```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      colorMode: false
    })
  ]
})
```


# Internationalization (i18n)

\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/vue
\> Looking for the Vue version?

## Usage

\> \[!NOTE]
\> See: /docs/components/app
\> Nuxt UI provides an App component that wraps your app to provide global configurations, including the \`locale\` prop.

### Locale

Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:

```vue [app.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>

<template>
  <UApp :locale="fr">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!TIP]
\> Each locale has a \`code\` property (e.g., \`en\`, \`en-GB\`, \`fr\`) that determines the date/time format in components like \[Calendar]\(/docs/components/calendar), \[InputDate]\(/docs/components/input-date) and \[InputTime]\(/docs/components/input-time).

### Custom locale

You can create your own locale using the [defineLocale](https://ui.nuxt.com/docs/composables/define-locale) utility:

```vue [app.vue]
<script setup lang="ts">
import type { Messages } from '@nuxt/ui'

const locale = defineLocale<Messages>({
  name: 'My custom locale',
  code: 'en',
  dir: 'ltr',
  messages: {
    // implement pairs
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!TIP]
\> Look at the \`code\` parameter, there you need to pass the iso code of the language. Example:\`hi\` Hindi (language)\`de-AT\`: German (language) as used in Austria (region)

### Extend locale

You can customize an existing locale by overriding its `messages` or `code` using the [extendLocale](https://ui.nuxt.com/docs/composables/extend-locale) utility:

```vue [app.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'

const locale = extendLocale(en, {
  code: 'en-AU',
  messages: {
    commandPalette: {
      placeholder: 'Search a component...'
    }
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

### Dynamic locale

To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/){rel="&#x22;nofollow&#x22;"} module.

Install the Nuxt I18n package
\`\`\`bash
pnpm add @nuxtjs/i18n
\`\`\`
\`\`\`bash
yarn add @nuxtjs/i18n
\`\`\`
\`\`\`bash
npm install @nuxtjs/i18n
\`\`\`
\`\`\`bash
bun add @nuxtjs/i18n
\`\`\`
Add the Nuxt I18n module in your nuxt.config.ts
\`\`\`ts
export default defineNuxtConfig({
modules: \[
'@nuxt/ui',
'@nuxtjs/i18n'
],
css: \['\~/assets/css/main.css'],
i18n: {
locales: \[{
code: 'de',
name: 'Deutsch'
}, {
code: 'en',
name: 'English'
}, {
code: 'fr',
name: 'Français'
}]
}
})
\`\`\`
Set the locale prop using useI18n
\`\`\`vue
\<script setup lang="ts">
import \* as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
\</script>
\<template>
\<UApp \:locale="locales\[locale]">
\<NuxtPage />
\</UApp>
\</template>
\`\`\`

### Dynamic direction

Each locale has a `dir` property which will be used by the `App` component to set the directionality of all components.

In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://nuxt.com/docs/api/composables/use-head){rel="&#x22;nofollow&#x22;"} composable:

```vue [app.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>
```

## Supported languages

\*See the full list of supported languages on the documentation website.\*


# Internationalization (i18n)

\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt
\> Looking for the Nuxt version?

## Usage

\> \[!NOTE]
\> See: /docs/components/app
\> Nuxt UI provides an App component that wraps your app to provide global configurations, including the \`locale\` prop.

### Locale

Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:

```vue [App.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>

<template>
  <UApp :locale="fr">
    <RouterView />
  </UApp>
</template>
```

\> \[!TIP]
\> Each locale has a \`code\` property (e.g., \`en\`, \`en-GB\`, \`fr\`) that determines the date/time format in components like \[Calendar]\(/docs/components/calendar), \[InputDate]\(/docs/components/input-date) and \[InputTime]\(/docs/components/input-time).

### Custom locale

You can create your own locale using the [defineLocale](https://ui.nuxt.com/docs/composables/define-locale) utility:

```vue [App.vue]
<script setup lang="ts">
import type { Messages } from '@nuxt/ui'
import { defineLocale } from '@nuxt/ui/composables'

const locale = defineLocale<Messages>({
  name: 'My custom locale',
  code: 'en',
  dir: 'ltr',
  messages: {
    // implement pairs
  }
})
</script>

<template>
  <UApp :locale="locale">
    <RouterView />
  </UApp>
</template>
```

\> \[!TIP]
\> Look at the \`code\` parameter, there you need to pass the iso code of the language. Example:\`hi\` Hindi (language)\`de-AT\`: German (language) as used in Austria (region)

### Extend locale

You can customize an existing locale by overriding its `messages` or `code` using the [extendLocale](https://ui.nuxt.com/docs/composables/extend-locale) utility:

```vue [App.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'
import { extendLocale } from '@nuxt/ui/composables'

const locale = extendLocale(en, {
  code: 'en-AU',
  messages: {
    commandPalette: {
      placeholder: 'Search a component...'
    }
  }
})
</script>

<template>
  <UApp :locale="locale">
    <RouterView />
  </UApp>
</template>
```

### Dynamic locale

To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/){rel="&#x22;nofollow&#x22;"} plugin.

Install the Vue I18n package
\`\`\`bash
pnpm add vue-i18n\@11
\`\`\`
\`\`\`bash
yarn add vue-i18n\@11
\`\`\`
\`\`\`bash
npm install vue-i18n\@11
\`\`\`
\`\`\`bash
bun add vue-i18n\@11
\`\`\`
Use the Vue I18n plugin in your main.ts
\`\`\`ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createI18n } from 'vue-i18n'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
routes: \[],
history: createWebHistory()
})
const i18n = createI18n({
legacy: false,
locale: 'en',
availableLocales: \['en', 'de'],
messages: {
en: {
// ...
},
de: {
// ...
}
}
})
app.use(router)
app.use(i18n)
app.use(ui)
app.mount('#app')
\`\`\`
Set the locale prop using useI18n
\`\`\`vue
\<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import \* as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
\</script>
\<template>
\<UApp \:locale="locales\[locale]">
\<RouterView />
\</UApp>
\</template>
\`\`\`

### Dynamic direction

Each locale has a `dir` property which will be used by the `App` component to set the directionality of all components.

In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://unhead.unjs.io/usage/composables/use-head){rel="&#x22;nofollow&#x22;"} composable:

```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useHead } from '@unhead/vue'
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]">
    <RouterView />
  </UApp>
</template>
```

## Supported languages

\*See the full list of supported languages on the documentation website.\*


# Content

## Installation

To get started, you can follow the official [guide](https://content.nuxt.com/docs/getting-started/installation){rel="&#x22;nofollow&#x22;"} or in summary:

\`\`\`bash
pnpm add @nuxt/content
\`\`\`
\`\`\`bash
yarn add @nuxt/content
\`\`\`
\`\`\`bash
npm install @nuxt/content
\`\`\`
\`\`\`bash
bun add @nuxt/content
\`\`\`

Then, add the `@nuxt/content` module in your `nuxt.config.ts`:

```ts [nuxt.config.ts] {4}
export default defineNuxtConfig({
  modules: [
    '@nuxt/ui',
    '@nuxt/content'
  ],
  css: ['~/assets/css/main.css']
})
```

\> \[!CAUTION]
\> You need to register \`@nuxt/content\` after \`@nuxt/ui\` in the \`modules\` array, otherwise the prose components will not be available.

## Configuration

When using Tailwind CSS classes in your markdown content files, you need to ensure Tailwind can detect and generate the necessary utility classes. By default, Tailwind's automatic content detection might not pick up classes written in markdown files.

To fix this, use the [`@source` directive](https://tailwindcss.com/docs/functions-and-directives#source-directive){rel="&#x22;nofollow&#x22;"} in your CSS file to explicitly include your content directory:

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";

@source "../../../content/**/*";
```

This ensures that:

- Tailwind scans all markdown files in your content directory
- Any utility classes used in your markdown (like `text-primary`) are included in the final CSS
- Dynamic classes in MDC components or custom Vue components within your content work properly

\> \[!TIP]
\> You can also use glob patterns to be more specific about which files to scan:\`@source "../../../content/docs/\*\*/\*.md"\` - Only scan markdown in the docs folder\`@source "../../../content/\*\*/\*.{md,yml}"\` - Include both markdown and YAML files

\> \[!NOTE]
\> See: https\://tailwindcss.com/docs/detecting-classes-in-source-files
\> Learn more about Tailwind's automatic content detection and best practices for optimizing build performance.

## Components

You might be using `@nuxt/content` to build a documentation. To help you with that, we've built some components that you can use in your pages:

- a built-in full-text search command palette with [ContentSearch](https://ui.nuxt.com/docs/components/content-search), replacing the need for Algolia DocSearch
- a navigation tree with the [ContentNavigation](https://ui.nuxt.com/docs/components/content-navigation) component
- a sticky Table of Contents with the [ContentToc](https://ui.nuxt.com/docs/components/content-toc) component
- a prev / next navigation with the [ContentSurround](https://ui.nuxt.com/docs/components/content-surround) component

## Typography

Nuxt UI provides its own custom implementations of all prose components for seamless integration with `@nuxt/content`. This approach ensures consistent styling, complete control over typography, and perfect alignment with the Nuxt UI design system so your content always looks and feels cohesive out of the box.

\> \[!NOTE]
\> See: /docs/typography
\> Discover the full Typography system and explore all available prose components for rich, consistent content presentation.

## Utils

### `mapContentNavigation`

This util will map the navigation from `queryCollectionNavigation` and transform it recursively into an array of objects that can be used by various components.

`mapContentNavigation(navigation, options?)`

- `navigation`: The navigation tree (array of ContentNavigationItem).
- `options`(optional):
  - `labelAttribute`: (string) Which field to use as label (`title` by default)
  - `deep`: (number or undefined) Controls how many levels of navigation are included (`undefined` by default : includes all levels)

**Example:** As shown in the breadcrumb example below, it's commonly used to transform the navigation data into the correct format.

```vue [app.vue]
<script setup lang="ts">
import { mapContentNavigation } from '@nuxt/ui/utils/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'

const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))

const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value?.path, { indexAsChild: true })).map(({ icon, ...link }) => link), { deep: 0 })
</script>

<template>
  <UPage>
    <UPageHeader v-bind="page">
      <template #headline>
        <UBreadcrumb :items="breadcrumb" />
      </template>
    </UPageHeader>
  </UPage>
</template>
```


# SSR

## Usage

When using Nuxt UI with Nuxt framework, SSR server will fully work out of the box. However, when using it with pure Vue, you will need to pay attention to some details to make it function as expected.

### Color Variables Injection

Nuxt UI, by default, injects to the `<head>` of the document color variables that are used across all components. Since the document is not managed by the UI library in the Vue SSR, you will need to inject that manually.

You can do that by using `@unhead` in the following way:

\`\`\`ts
import { createHead, renderSSRHead } from '@unhead/vue/server'
// Create the header with unhead
const head = createHead()
// Render SSR header and append it to the SSR application instance
const payload = await renderSSRHead(head)
app.head.push(payload.headTags)
\`\`\`
\`\`\`ts
import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import ui from '@nuxt/ui/vue-plugin'
import { createHead, renderSSRHead } from '@unhead/vue/server'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { createSSRApp, h } from 'vue'
import { renderToString } from 'vue/server-renderer'
import type { DefineComponent } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'Laravel x Nuxt UI'
createServer(
(page) => {
const head = createHead()
return createInertiaApp({
page,
render: renderToString,
title: (title) => (title ? \`${title} - ${appName}\` : appName),
resolve: (name) =>
resolvePageComponent(
\`./pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('./pages/\*\*/\*.vue')
),
setup: ({ App, props, plugin }) =>
createSSRApp({ render: () => h(App, props) })
.use(plugin)
.use(head)
.use(ui)
}).then(async (app) => {
const payload = await renderSSRHead(head)
app.head.push(payload.headTags)
return app
})
},
{ cluster: true }
)
\`\`\`
\`\`\`ts
import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import ui from '@nuxt/ui/vue-plugin'
import { createHead, renderSSRHead } from '@unhead/vue/server'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
import { createSSRApp, h } from 'vue'
import { renderToString } from 'vue/server-renderer'
import type { DefineComponent } from 'vue'
const appName = import.meta.env.VITE\_APP\_NAME || 'AdonisJS x Nuxt UI'
createServer(
(page) => {
const head = createHead()
return createInertiaApp({
page,
render: renderToString,
title: (title) => (title ? \`${title} - ${appName}\` : appName),
resolve: (name) =>
resolvePageComponent(
\`../pages/${name}.vue\`,
import.meta.glob\<DefineComponent>('../pages/\*\*/\*.vue')
),
setup: ({ App, props, plugin }) =>
createSSRApp({ render: () => h(App, props) })
.use(plugin)
.use(head)
.use(ui)
}).then(async (app) => {
const payload = await renderSSRHead(head)
app.head.push(payload.headTags)
return app
})
},
{ cluster: true }
)
\`\`\`

### Color Scheme Detection

The same goes to the color scheme detection. To avoid flashings in the SSR because of the selected color scheme difference, you will need to detect the user's color scheme before the application initialization.

Adding the script below to the `<head>` of your document will detect if the user is using dark theme, and therefore, render the SSR in the dark theme as well.

\`\`\`html
\<script>
const theme = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (theme === 'dark' || (theme === 'auto' && window\.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
\</script>
\`\`\`
\`\`\`html
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead
@vite('resources/js/app.ts')
\<script>
const theme = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (theme === 'dark' || (theme === 'auto' && window\.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
\</script>
\</head>
\<body>
\<div class="isolate">
@inertia
\</div>
\</body>
\</html>
\`\`\`
\`\`\`html
\<!DOCTYPE html>
\<html>
\<head>
\<meta charset="utf-8" />
\<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead()
@vite(\['inertia/app/app.ts', \`inertia/pages/${page.component}.vue\`])
\<script>
const theme = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (theme === 'dark' || (theme === 'auto' && window\.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
\</script>
\</head>
\<body>
@inertia({ class: 'isolate' })
\</body>
\</html>
\`\`\`

### Icons Display

Unfortunately displaying icons with the SSR server of the Vue version is currently not supported. The icons will only be displayed after the application is initialized at the user's end.


# MCP Server

## What is MCP?

MCP (Model Context Protocol) is a standardized protocol that enables AI assistants to access external data sources and tools. Nuxt UI provides an MCP server that allows AI assistants like Claude Code, Cursor, and Windsurf to access component information, source code, and usage examples directly.

The MCP server provides structured access to our component library, making it easy for AI tools to understand and assist with Nuxt UI development.

## Available Resources

The Nuxt UI MCP server provides the following resources for discovery:

- **`resource://nuxt-ui/components`**: Browse all available components with categories
- **`resource://nuxt-ui/composables`**: Browse all available composables with categories
- **`resource://nuxt-ui/examples`**: Browse all available code examples
- **`resource://nuxt-ui/templates`**: Browse all available project templates
- **`resource://nuxt-ui/documentation-pages`**: Browse all available documentation pages

You're able to access these resources with tools like Claude Code by using `@`.

## Available Tools

The Nuxt UI MCP server provides the following tools organized by category:

### Component Tools

- **`list_components`**: Lists all available Nuxt UI components with their categories and basic information
- **`list_composables`**: Lists all available Nuxt UI composables with their categories and basic information
- **`get_component`**: Retrieves component documentation and details. Supports a `sections` parameter (`usage`, `examples`, `api`, `theme`, `changelog`) to fetch only specific parts and reduce response size
- **`get_component_metadata`**: Retrieves detailed metadata for a component including props, slots, and events (lightweight, no documentation content)
- **`search_components_by_category`**: Searches components by category or text filter

### Template Tools

- **`list_templates`**: Lists all available Nuxt UI templates with optional category filtering
- **`get_template`**: Retrieves template details and setup instructions

### Documentation Tools

- **`list_documentation_pages`**: Lists all documentation pages
- **`get_documentation_page`**: Retrieves documentation page content by URL path. Supports a `sections` parameter to fetch only specific h2 sections (e.g., `["Usage", "API"]`) and reduce response size
- **`list_getting_started_guides`**: Lists all getting started guides and installation instructions

### Example Tools

- **`list_examples`**: Lists all available UI examples and code demonstrations
- **`get_example`**: Retrieves specific UI example implementation code and details

### Migration Tools

- **`get_migration_guide`**: Retrieves version-specific migration guides and upgrade instructions

## Available Prompts

The Nuxt UI MCP server provides guided prompts for common workflows:

- **`find_component_for_usecase`**: Find the best component for your specific use case
- **`implement_component_with_props`**: Generate complete component implementation with proper props
- **`setup_project_with_template`**: Get guided setup instructions for project templates

You're able to access these resources with tools like Claude Code by using `/`.

## Configuration

The Nuxt UI MCP server uses HTTP transport and can be configured in different AI assistants.

### ChatGPT

\> \[!NOTE]
\> Custom connectors using MCP are available on ChatGPT for Pro and Plus accounts. Accessible on the web.

Follow these steps to set up Nuxt UI as a connector within ChatGPT:

1. **Enable Developer mode:**
   - Go to "Settings" > "Connectors" > "Advanced settings" > "Developer mode"
2. **Open ChatGPT settings**
3. **In the Connectors tab, create a new connector:**
   - Give it a name: `Nuxt UI`
   - MCP server URL: `https://ui.nuxt.com/mcp`
   - Authentication: `None`
4. **Click Create**

The Nuxt UI connector will appear in the composer's "Developer mode" tool later during conversations.

### Claude Code

\> \[!NOTE]
\> Ensure Claude Code is installed. Visit \[Anthropic's documentation]\(https\://docs.anthropic.com/en/docs/claude-code/quickstart) for installation instructions.

Add the server using the CLI command:

```bash
claude mcp add --transport http nuxt-ui-remote https://ui.nuxt.com/mcp
```

### Claude Desktop

#### Setup Instructions:

1. Open Claude Desktop and navigate to "Settings" > "Developer".
2. Click on "Edit Config". This will open the local Claude directory.
3. Modify the `claude_desktop_config.json` file with your custom MCP server configuration.

```json [claude_desktop_config.json]
{
  "mcpServers": {
    "nuxt-ui": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://ui.nuxt.com/mcp"
      ]
    }
  }
}
```

4. Restart Claude Desktop app. The Nuxt UI MCP server should now be registered.

### Cursor

#### Quick Install

Click the button below to install the Nuxt UI MCP server directly in Cursor:

\[Install MCP Server]\(cursor://anysphere.cursor-deeplink/mcp/install?name=nuxt-ui\&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vdWkubnV4dC5jb20vbWNwIn0%3D)

#### Manual Setup Instructions:

1. Open Cursor and go to "Settings" > "Tools & MCP"
2. Add the Nuxt UI MCP server configuration

Or manually create/update `.cursor/mcp.json` in your project root:

```json [.cursor/mcp.json]
{
  "mcpServers": {
    "nuxt-ui": {
      "type": "http",
      "url": "https://ui.nuxt.com/mcp"
    }
  }
}
```

### Google Antigravity

#### Setup Instructions:

1. Open the MCP store via the "..." dropdown at the top of the editor's agent panel.
2. Click on "Manage MCP Servers"
3. Click on "View raw config"
4. Modify the `mcp_config.json` with your custom MCP server configuration:

```json
{
  "mcpServers": {
    "nuxt-ui": {
      "serverUrl": "https://ui.nuxt.com/mcp"
    }
  }
}
```

5. Return to the "Manage MCP Servers" tab and click "Refresh". The Nuxt UI MCP server should now appear.

### Gemini CLI

#### Setup Instructions:

1. Locate your Gemini CLI configuration file (usually \~/.gemini/settings.json or as specified in your environment).
2. Add the following configuration to your mcpServers object:

```json
{
  "mcpServers": {
    "nuxt-ui": {
      "url": "https://ui.nuxt.com/mcp"
    }
  }
}
```

3. Restart your terminal session or reload the CLI. The Nuxt UI MCP server tools will now be available for use.

### Le Chat Mistral

#### Setup Instructions:

1. Navigate to "Intelligence" > "Connectors"
2. Click on "Add Connector" button, then select "Custom MCP Connector"
3. Create your Custom MCP Connector:
   - Connector Name: `NuxtUI`
   - Connector Server: `https://ui.nuxt.com/mcp`

### Visual Studio Code

\> \[!NOTE]
\> Install required extensions. Ensure you have \[GitHub Copilot]\(https\://marketplace.visualstudio.com/items?itemName=GitHub.copilot) and \[GitHub Copilot Chat]\(https\://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extensions installed.

#### Setup Instructions:

1. Open VS Code and access the Command Palette (Ctrl/Cmd + Shift + P)
2. Type "Preferences: Open Workspace Settings (JSON)" and select it
3. Navigate to your project's `.vscode` folder or create one if it doesn't exist
4. Create or edit the `mcp.json` file with the following configuration:

```json [.vscode/mcp.json]
{
  "servers": {
    "nuxt-ui": {
      "type": "http",
      "url": "https://ui.nuxt.com/mcp"
    }
  }
}
```

### Windsurf

#### Setup Instructions:

1. Open Windsurf and navigate to "Settings" > "Windsurf Settings" > "Cascade"
2. Click the "Manage MCPs" button, then select the "View raw config" option
3. Add the following configuration to your MCP settings:

```json [.codeium/windsurf/mcp_config.json]
{
  "mcpServers": {
    "nuxt-ui": {
      "type": "http",
      "url": "https://ui.nuxt.com/mcp"
    }
  }
}
```

### Zed

#### Setup Instructions:

1. Open Zed and go to "Settings" > "Open Settings"
2. Navigate to the JSON settings file
3. Add the following context server configuration to your settings:

```json [.config/zed/settings.json]
{
  "context_servers": {
    "nuxt-ui": {
      "source": "custom",
      "command": "npx",
      "args": ["mcp-remote", "https://ui.nuxt.com/mcp"],
      "env": {}
    }
  }
}
```

### Opencode

#### Setup Instructions:

1. In your project root, create `opencode.json`
2. Add the following configuration:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "nuxt-ui": {
      "type": "remote",
      "url": "https://ui.nuxt.com/mcp",
      "enabled": true
    }
  }
}
```

### GitHub Copilot Agent

\> \[!NOTE]
\> Repository administrator access required. This is needed to configure MCP servers for GitHub Copilot coding agent.

If you have already configured MCP servers in VS Code (replace the `servers` key with `mcpServers` for GitHub Copilot Agent), you can leverage a similar configuration for GitHub Copilot coding agent. You will need to add a `tools` key specifying which tools are available to Copilot.

#### Setup Instructions:

1. Navigate to your GitHub repository
2. Go to **Settings** > **Code & automation** > **Copilot** > **Coding agent**
3. In the **MCP configuration** section, add the following configuration:

```json
{
  "mcpServers": {
    "nuxt-ui": {
      "type": "http",
      "url": "https://ui.nuxt.com/mcp",
      "tools": ["*"]
    }
  }
}
```

4. Click Save

### Validating the Configuration

To verify the MCP server is configured correctly:

1. Create an issue in your repository and assign it to Copilot
2. Wait for Copilot to create a pull request
3. In the pull request, click View session in the "Copilot started work" timeline event
4. Click the ellipsis button (...) at the top right, then click Copilot in the sidebar
5. Expand the Start MCP Servers step to see the configured Nuxt tools

For more information on using MCP with GitHub Copilot coding agent, see [Extend coding agent with MCP](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp){rel="&#x22;nofollow&#x22;"}.

## Usage Examples

Once configured, you can ask your AI assistant questions like:

- "List all available Nuxt UI components"
- "Get Button component documentation"
- "Show me just the Button usage examples"
- "What props does Input accept?"
- "Find form-related components"
- "List dashboard templates"
- "Get template setup instructions"
- "Show installation guide"
- "Get v4 migration guide"
- "List all examples"
- "Get ContactForm example code"

The AI assistant will use the MCP server to fetch structured JSON data and provide guided assistance for Nuxt UI during development.

\> \[!TIP]
\> For large documentation pages, the AI can use the \`sections\` parameter to fetch only relevant parts (like "Usage" or "API"), reducing response size and improving performance.


# LLMs.txt

## What is LLMs.txt?

LLMs.txt is a structured documentation format specifically designed for large language models (LLMs). Nuxt UI provides LLMs.txt files that contain comprehensive information about our component library, making it easy for AI tools to understand and assist with Nuxt UI development.

These files are optimized for AI consumption and contain structured information about components, APIs, usage patterns, and best practices.

## Available routes

We provide LLMs.txt routes to help AI tools access our documentation:

- **`/llms.txt`** - Contains a structured overview of all components and their documentation links (\~5K tokens)
- **`/llms-full.txt`** - Provides comprehensive documentation including implementation details, examples, theming, composables, and migration guidance (\~1M+ tokens)

## Choosing the Right File

\> \[!NOTE]
\> Most users should start with \`/llms.txt\` - it contains all essential information and works with standard LLM context windows. Use \`/llms-full.txt\` only if you need comprehensive implementation examples and your AI tool supports large contexts (200K+ tokens).

## Important usage notes

\> \[!WARNING]
\> @-symbol must be typed manually - When using tools like Cursor or Windsurf, the \`@\` symbol must be typed by hand in the chat interface. Copy-pasting breaks the tool's ability to recognize it as a context reference.

## Usage with AI Tools

### Cursor

Nuxt UI provides specialized LLMs.txt files that you can reference in Cursor for better AI assistance with component development.

#### How to use:

1. **Direct reference**: Mention the LLMs.txt URLs when asking questions
2. Add these specific URLs to your project context using `@docs`

[Read more about Cursor Web and Docs Search](https://docs.cursor.com/en/context/@-symbols/@-docs){rel="&#x22;nofollow&#x22;"}

### Windsurf

Windsurf can directly access the Nuxt UI LLMs.txt files to understand component usage and best practices.

#### Using LLMs.txt with Windsurf:

- Use `@docs` to reference specific LLMs.txt URLs
- Create persistent rules referencing these URLs in your workspace

[Read more about Windsurf Web and Docs Search](https://docs.windsurf.com/windsurf/cascade/web-search){rel="&#x22;nofollow&#x22;"}

### Other AI Tools

Any AI tool that supports LLMs.txt can use these routes to better understand Nuxt UI.

#### Examples for ChatGPT, Claude, or other LLMs:

- "Using Nuxt UI documentation from <https://ui.nuxt.com/llms.txt>{rel="&#x22;nofollow&#x22;"}"
- "Follow complete Nuxt UI guidelines from <https://ui.nuxt.com/llms-full.txt>{rel="&#x22;nofollow&#x22;"}"


# Skills

## What are Skills?

Skills are structured knowledge files that give AI coding agents context about a library, framework, or codebase. Unlike MCP servers that provide real-time tool access, skills are loaded directly into the agent's context so it can reference them throughout the conversation.

Nuxt UI provides two skills:

- **Usage skill** — Teaches AI agents how to build UIs with Nuxt UI: installation, theming, components, composables, forms, overlays, and layouts.
- **Contributing skill** — Guides AI agents through Nuxt UI's component structure, theming patterns, testing conventions, and documentation guidelines when contributing to the repository.

## Usage Skill

The usage skill gives AI agents comprehensive knowledge about building with Nuxt UI v4. It covers:

- Installation for Nuxt, Vue, Laravel, and AdonisJS
- Theming and branding with semantic colors and CSS variables
- All 125+ components with props and usage patterns
- Composables (`useToast`, `useOverlay`, `defineShortcuts`)
- Form validation with Standard Schema
- Layout composition (Dashboard, Docs, Chat, Editor)
- Official starter templates

The skill includes additional references that the AI agent loads on demand based on the task, keeping responses focused and context-efficient.

\> \[!NOTE]
\> Once installed, you can invoke the skill by typing \`/nuxt-ui\` in your agent's chat.

### Skills CLI

The [`skills`](https://skills.sh){rel="&#x22;nofollow&#x22;"} CLI is the easiest way to install the Nuxt UI skill. It supports 35+ agents including Cursor, Claude Code, Codex, Windsurf, Cline, and more.

```bash
npx skills add nuxt/ui
```

You can target specific agents with the `--agent` flag:

```bash
npx skills add nuxt/ui --agent cursor
npx skills add nuxt/ui --agent claude-code
```

Or install globally so the skill is available across all your projects:

```bash
npx skills add nuxt/ui --global
```

\> \[!TIP]
\> Visit \[skills.sh]\(https\://skills.sh) to learn more about the skills ecosystem and browse available skills.

### Cursor

#### Quick Install

Click the button below to install the Nuxt UI skill directly in Cursor:

\[Install Skill]\(cursor://anysphere.cursor-deeplink/install-skill?url=https\://github.com/nuxt/ui/tree/v4/skills/nuxt-ui)

#### Manual Setup

1. Open Cursor and go to "Settings" > "Skills"
2. Click "Add skill" and enter the following URL:

```text
https://github.com/nuxt/ui/tree/v4/skills/nuxt-ui
```

### Claude Code

\> \[!NOTE]
\> Ensure Claude Code is installed. Visit \[Anthropic's documentation]\(https\://docs.anthropic.com/en/docs/claude-code/quickstart) for installation instructions.

Add the skill using the CLI command:

```bash
claude skill add https://github.com/nuxt/ui/tree/v4/skills/nuxt-ui
```

### Other AI Tools

The skill files are publicly available on GitHub. You can reference them directly in any AI tool that supports custom context or instructions:

- **Skill entry point**: [`skills/nuxt-ui/SKILL.md`](https://github.com/nuxt/ui/tree/v4/skills/nuxt-ui/SKILL.md){rel="&#x22;nofollow&#x22;"}
- **Full skill directory**: [`skills/nuxt-ui/`](https://github.com/nuxt/ui/tree/v4/skills/nuxt-ui){rel="&#x22;nofollow&#x22;"}

## Contributing Skill

The contributing skill helps AI agents assist with contributions to the Nuxt UI repository. It automatically guides through:

- Component file structure and naming conventions
- Tailwind Variants theming patterns
- Vitest testing conventions
- MDC documentation guidelines

This skill is located in the repository at [`.claude/skills/contributing/`](https://github.com/nuxt/ui/tree/v4/.claude/skills/contributing){rel="&#x22;nofollow&#x22;"} and is automatically picked up when working in the Nuxt UI codebase with Claude Code or Cursor (when using a Claude model). You can also invoke it by typing `/contributing` in your agent's chat.

\> \[!TIP]
\> For more details on contributing to Nuxt UI, see the \[Contribution guide]\(/docs/getting-started/contribution#ai-assistance).


# Accordion

## Usage

Use the Accordion component to display a list of collapsible items.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Is Nuxt UI free to use?',
    content: 'Yes! Nuxt UI is completely free and open source under the MIT license. All 125+ components are available to everyone.',
  },
  {
    label: 'Can I use Nuxt UI with Vue without Nuxt?',
    content: 'Yes! While optimized for Nuxt, Nuxt UI works perfectly with standalone Vue projects via our Vite plugin. You can follow the [installation guide](/docs/getting-started/installation/vue) to get started.',
  },
  {
    label: 'Is Nuxt UI production-ready?',
    content: 'Yes! Nuxt UI is used in production by thousands of applications with extensive tests, regular updates, and active maintenance.',
  },
])
</script>

<template>
  <UAccordion :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `trailingIcon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `content?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `value?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, header?: ClassNameValue, trigger?: ClassNameValue, leadingIcon?: ClassNameValue, label?: ClassNameValue, trailingIcon?: ClassNameValue, content?: ClassNameValue, body?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion :items="items" />
</template>
```

### Multiple

Set the `type` prop to `multiple` to allow multiple items to be active at the same time. Defaults to `single`.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion type="multiple" :items="items" />
</template>
```

### Collapsible

When `type` is `single`, you can set the `collapsible` prop to `false` to prevent the active item from collapsing.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion :collapsible="false" :items="items" />
</template>
```

### Unmount

Use the `unmount-on-hide` prop to prevent the content from being unmounted when the accordion is collapsed. Defaults to `true`.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion :unmount-on-hide="false" :items="items" />
</template>
```

\> \[!NOTE]
\> You can inspect the DOM to see each item's content being rendered.

### Disabled

Use the `disabled` property to disable the Accordion.

You can also disable a specific item by using the `disabled` property in the item object.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
    disabled: true,
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion disabled :items="items" />
</template>
```

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon) of each item. Defaults to `i-lucide-chevron-down`.

\> \[!TIP]
\> You can also set an icon for a specific item by using the \`trailingIcon\` property in the item object.

```vue
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = ref<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.',
    trailingIcon: 'i-lucide-plus',
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
  },
])
</script>

<template>
  <UAccordion trailing-icon="i-lucide-arrow-down" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

## Examples

### Control active item(s)

You can control the active item by using the `default-value` prop or the `v-model` directive with the `value` of the item. If no `value` is provided, it defaults to the index **as a string**.

```vue [AccordionModelValueExample.vue]
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items: AccordionItem[] = [
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.'
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
  }
]

const active = ref('0')

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    active.value = String((Number(active.value) + 1) % items.length)
  }, 2000)
})
</script>

<template>
  <UAccordion v-model="active" :items="items" />
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to change the key used to match items when a \`v-model\` or \`default-value\` is provided.

\> \[!CAUTION]
\> When \`type="multiple"\`, ensure to pass an array to the \`default-value\` prop or the \`v-model\` directive.

### With drag and drop

Use the [`useSortable`](https://vueuse.org/integrations/useSortable/){rel="&#x22;nofollow&#x22;"} composable from [`@vueuse/integrations`](https://vueuse.org/integrations/README.html){rel="&#x22;nofollow&#x22;"} to enable drag and drop functionality on the Accordion. This integration wraps [Sortable.js](https://sortablejs.github.io/Sortable/){rel="&#x22;nofollow&#x22;"} to provide a seamless drag and drop experience.

```vue [AccordionDragAndDropExample.vue]
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'

const items = shallowRef<AccordionItem[]>([
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.'
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
  }
])

const accordion = useTemplateRef<HTMLElement>('accordion')

useSortable(accordion, items, {
  animation: 150
})
</script>

<template>
  <UAccordion ref="accordion" :items="items" />
</template>
```

### With body slot

Use the `#body` slot to customize the body of each item.

```vue [AccordionBodySlotExample.vue]
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items: AccordionItem[] = [
  {
    label: 'Icons',
    icon: 'i-lucide-smile'
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box'
  }
]
</script>

<template>
  <UAccordion :items="items">
    <template #body="{ item }">
      This is the {{ item.label }} panel.
    </template>
  </UAccordion>
</template>
```

\> \[!TIP]
\> The \`#body\` slot includes some pre-defined styles, use the \[\`#content\` slot]\(#with-content-slot) if you want to start from scratch.

### With content slot

Use the `#content` slot to customize the content of each item.

```vue [AccordionContentSlotExample.vue]
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items: AccordionItem[] = [
  {
    label: 'Icons',
    icon: 'i-lucide-smile'
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box'
  }
]
</script>

<template>
  <UAccordion :items="items">
    <template #content="{ item }">
      <p class="pb-3.5 text-sm text-muted">
        This is the {{ item.label }} panel.
      </p>
    </template>
  </UAccordion>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-body`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [AccordionCustomSlotExample.vue]
<script setup lang="ts">
import type { AccordionItem } from '@nuxt/ui'

const items = [
  {
    label: 'Icons',
    icon: 'i-lucide-smile',
    content: 'You have nothing to do, @nuxt/icon will handle it automatically.'
  },
  {
    label: 'Colors',
    icon: 'i-lucide-swatch-book',
    slot: 'colors' as const,
    content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
  }
] satisfies AccordionItem[]
</script>

<template>
  <UAccordion :items="items">
    <template #colors="{ item }">
      <p class="text-sm pb-3.5 text-primary">
        {{ item.content }}
      </p>
    </template>
  </UAccordion>
</template>
```

### With markdown content

You can use the [MDC](https://github.com/nuxt-modules/mdc?tab=readme-ov-file#mdc){rel="&#x22;nofollow&#x22;"} component from `@nuxtjs/mdc` to render markdown in the accordion items.

```vue [AccordionMarkdownExample.vue]
<script setup lang="ts">
const items = [
  {
    label: 'Is Nuxt UI free to use?',
    icon: 'i-lucide-circle-help',
    content: 'Yes! Nuxt UI is completely free and open source under the MIT license. All 125+ components are available to everyone.'
  },
  {
    label: 'Can I use Nuxt UI with Vue without Nuxt?',
    icon: 'i-lucide-circle-help',
    content: 'Yes! While optimized for Nuxt, Nuxt UI works perfectly with standalone Vue projects via our Vite plugin. You can follow the [installation guide](/docs/getting-started/installation/vue) to get started.'
  },
  {
    label: 'Will Nuxt UI work with other CSS frameworks like UnoCSS?',
    icon: 'i-lucide-circle-help',
    content: 'No. Nuxt UI is designed exclusively for Tailwind CSS. UnoCSS support would require significant architecture changes due to different class naming conventions.'
  },
  {
    label: 'How does Nuxt UI handle accessibility?',
    icon: 'i-lucide-circle-help',
    content: 'Through [Reka UI](https://reka-ui.com/docs/overview/accessibility) integration, Nuxt UI provides automatic ARIA attributes, keyboard navigation, focus management, and screen reader support. While offering a strong foundation, testing in your specific use case remains important.'
  },
  {
    label: 'How is Nuxt UI tested?',
    icon: 'i-lucide-circle-help',
    content: 'Nuxt UI ensures reliability with 1000+ Vitest tests covering core functionality and accessibility.'
  },
  {
    label: 'Is Nuxt UI production-ready?',
    icon: 'i-lucide-circle-help',
    content: 'Yes! Nuxt UI is used in production by thousands of applications with extensive tests, regular updates, and active maintenance.'
  }
]
</script>

<template>
  <UAccordion
    type="multiple"
    :items="items"
    :unmount-on-hide="false"
    :default-value="['3']"
    :ui="{
      trigger: 'text-base',
      body: 'text-base text-muted'
    }"
  >
    <template #body="{ item }">
      <MDC :value="item.content" unwrap="p" />
    </template>
  </UAccordion>
</template>
```

## API

### Props

```ts
/**
 * Props for the Accordion component
 */
interface AccordionProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  items?: T[] | undefined;
  /**
   * The icon displayed on the right side of the trigger.
   */
  trailingIcon?: any;
  /**
   * The key used to get the value from the item.
   * @default "\"value\""
   */
  valueKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  ui?: { root?: ClassNameValue; item?: ClassNameValue; header?: ClassNameValue; trigger?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; leadingIcon?: ClassNameValue; trailingIcon?: ClassNameValue; label?: ClassNameValue; } | undefined;
  /**
   * When type is "single", allows closing content when clicking trigger for an open item.
   * When type is "multiple", this prop has no effect.
   * @default "true"
   */
  collapsible?: boolean | undefined;
  /**
   * The default active value of the item(s).
   * 
   * Use when you do not need to control the state of the item(s).
   */
  defaultValue?: string | string[] | undefined;
  /**
   * The controlled value of the active item(s).
   * 
   * Use this when you need to control the state of the items. Can be binded with `v-model`
   */
  modelValue?: string | string[] | undefined;
  /**
   * Determines whether a "single" or "multiple" items can be selected at a time.
   * 
   * This prop will overwrite the inferred type from `modelValue` and `defaultValue`.
   * @default "\"single\""
   */
  type?: SingleOrMultipleType | undefined;
  /**
   * When `true`, prevents the user from interacting with the accordion and all its items
   */
  disabled?: boolean | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "true"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Accordion component
 */
interface AccordionSlots {
  default(): any;
  leading(): any;
  trailing(): any;
  content(): any;
  body(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Accordion component
 */
interface AccordionEmits {
  update:modelValue: (payload: [value: string | string[] | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    accordion: {
      slots: {
        root: 'w-full',
        item: 'border-b border-default last:border-b-0',
        header: 'flex',
        trigger: 'group flex-1 flex items-center gap-1.5 font-medium text-sm py-3.5 focus-visible:outline-primary min-w-0',
        content: 'data-[state=open]:animate-[accordion-down_200ms_ease-out] data-[state=closed]:animate-[accordion-up_200ms_ease-out] overflow-hidden focus:outline-none',
        body: 'text-sm pb-3.5',
        leadingIcon: 'shrink-0 size-5',
        trailingIcon: 'shrink-0 size-5 ms-auto group-data-[state=open]:rotate-180 transition-transform duration-200',
        label: 'text-start break-words'
      },
      variants: {
        disabled: {
          true: {
            trigger: 'cursor-not-allowed opacity-75'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Accordion.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/accordion.ts).


# Alert

## Usage

### Title

Use the `title` prop to set the title of the Alert.

```vue
<template>
  <UAlert title="Heads up!" />
</template>
```

### Description

Use the `description` prop to set the description of the Alert.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon).

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." icon="i-lucide-terminal" />
</template>
```

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar).

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." />
</template>
```

### Color

Use the `color` prop to change the color of the Alert.

```vue
<template>
  <UAlert color="neutral" title="Heads up!" description="You can change the primary color in your app config." icon="i-lucide-terminal" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the Alert.

```vue
<template>
  <UAlert color="neutral" variant="subtle" title="Heads up!" description="You can change the primary color in your app config." icon="i-lucide-terminal" />
</template>
```

### Close

Use the `close` prop to display a [Button](https://ui.nuxt.com/docs/components/button) to dismiss the Alert.

\> \[!TIP]
\> An \`update\:open\` event will be emitted when the close button is clicked.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." color="neutral" variant="outline" close />
</template>
```

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." color="neutral" variant="outline" />
</template>
```

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." color="neutral" variant="outline" close close-icon="i-lucide-arrow-right" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Actions

Use the `actions` prop to add some [Button](https://ui.nuxt.com/docs/components/button) actions to the Alert.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." color="neutral" variant="outline" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Alert.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." color="neutral" variant="outline" orientation="horizontal" />
</template>
```

## Examples

### `class` prop

Use the `class` prop to override the base styles of the Alert.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." class="rounded-none" />
</template>
```

### `ui` prop

Use the `ui` prop to override the slots styles of the Alert.

```vue
<template>
  <UAlert title="Heads up!" description="You can change the primary color in your app config." icon="i-lucide-rocket" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Alert component
 */
interface AlertProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  icon?: any;
  avatar?: AvatarProps | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | undefined;
  /**
   * The orientation between the content and the actions.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Display a list of actions:
   * - under the title and description when orientation is `vertical`
   * - next to the close button when orientation is `horizontal`
   * `{ size: 'xs' }`{lang="ts-type"}
   */
  actions?: ButtonProps[] | undefined;
  /**
   * Display a close button to dismiss the alert.
   * `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  ui?: { root?: ClassNameValue; wrapper?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; icon?: ClassNameValue; avatar?: ClassNameValue; avatarSize?: ClassNameValue; actions?: ClassNameValue; close?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Alert component
 */
interface AlertSlots {
  leading(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Alert component
 */
interface AlertEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    alert: {
      slots: {
        root: 'relative overflow-hidden w-full rounded-lg p-4 flex gap-2.5',
        wrapper: 'min-w-0 flex-1 flex flex-col',
        title: 'text-sm font-medium',
        description: 'text-sm opacity-90',
        icon: 'shrink-0 size-5',
        avatar: 'shrink-0',
        avatarSize: '2xl',
        actions: 'flex flex-wrap gap-1.5 shrink-0',
        close: 'p-0'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        orientation: {
          horizontal: {
            root: 'items-center',
            actions: 'items-center'
          },
          vertical: {
            root: 'items-start',
            actions: 'items-start mt-2.5'
          }
        },
        title: {
          true: {
            description: 'mt-1'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: {
            root: 'bg-primary text-inverted'
          }
        },
        {
          color: 'secondary',
          variant: 'solid',
          class: {
            root: 'bg-secondary text-inverted'
          }
        },
        {
          color: 'success',
          variant: 'solid',
          class: {
            root: 'bg-success text-inverted'
          }
        },
        {
          color: 'info',
          variant: 'solid',
          class: {
            root: 'bg-info text-inverted'
          }
        },
        {
          color: 'warning',
          variant: 'solid',
          class: {
            root: 'bg-warning text-inverted'
          }
        },
        {
          color: 'error',
          variant: 'solid',
          class: {
            root: 'bg-error text-inverted'
          }
        },
        {
          color: 'primary',
          variant: 'outline',
          class: {
            root: 'text-primary ring ring-inset ring-primary/25'
          }
        },
        {
          color: 'secondary',
          variant: 'outline',
          class: {
            root: 'text-secondary ring ring-inset ring-secondary/25'
          }
        },
        {
          color: 'success',
          variant: 'outline',
          class: {
            root: 'text-success ring ring-inset ring-success/25'
          }
        },
        {
          color: 'info',
          variant: 'outline',
          class: {
            root: 'text-info ring ring-inset ring-info/25'
          }
        },
        {
          color: 'warning',
          variant: 'outline',
          class: {
            root: 'text-warning ring ring-inset ring-warning/25'
          }
        },
        {
          color: 'error',
          variant: 'outline',
          class: {
            root: 'text-error ring ring-inset ring-error/25'
          }
        },
        {
          color: 'primary',
          variant: 'soft',
          class: {
            root: 'bg-primary/10 text-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'soft',
          class: {
            root: 'bg-secondary/10 text-secondary'
          }
        },
        {
          color: 'success',
          variant: 'soft',
          class: {
            root: 'bg-success/10 text-success'
          }
        },
        {
          color: 'info',
          variant: 'soft',
          class: {
            root: 'bg-info/10 text-info'
          }
        },
        {
          color: 'warning',
          variant: 'soft',
          class: {
            root: 'bg-warning/10 text-warning'
          }
        },
        {
          color: 'error',
          variant: 'soft',
          class: {
            root: 'bg-error/10 text-error'
          }
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: {
            root: 'bg-primary/10 text-primary ring ring-inset ring-primary/25'
          }
        },
        {
          color: 'secondary',
          variant: 'subtle',
          class: {
            root: 'bg-secondary/10 text-secondary ring ring-inset ring-secondary/25'
          }
        },
        {
          color: 'success',
          variant: 'subtle',
          class: {
            root: 'bg-success/10 text-success ring ring-inset ring-success/25'
          }
        },
        {
          color: 'info',
          variant: 'subtle',
          class: {
            root: 'bg-info/10 text-info ring ring-inset ring-info/25'
          }
        },
        {
          color: 'warning',
          variant: 'subtle',
          class: {
            root: 'bg-warning/10 text-warning ring ring-inset ring-warning/25'
          }
        },
        {
          color: 'error',
          variant: 'subtle',
          class: {
            root: 'bg-error/10 text-error ring ring-inset ring-error/25'
          }
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: {
            root: 'text-inverted bg-inverted'
          }
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: {
            root: 'text-highlighted bg-default ring ring-inset ring-default'
          }
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: {
            root: 'text-highlighted bg-elevated/50'
          }
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: {
            root: 'text-highlighted bg-elevated/50 ring ring-inset ring-accented'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'solid'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Alert.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/alert.ts).


# App

## Usage

This component implements Reka UI [ConfigProvider](https://reka-ui.com/docs/utilities/config-provider){rel="&#x22;nofollow&#x22;"} to provide global configuration to all components:

- Enables all primitives to inherit global reading direction.
- Enables changing the behavior of scroll body when setting body lock.
- Much more controls to prevent layout shifts.

It's also using [ToastProvider](https://reka-ui.com/docs/components/toast#provider){rel="&#x22;nofollow&#x22;"} and [TooltipProvider](https://reka-ui.com/docs/components/tooltip#provider){rel="&#x22;nofollow&#x22;"} to provide global toasts and tooltips, as well as programmatic modals and slideovers.

Wrap your entire application with the App component in your `app.vue` file:

```vue [app.vue]
<template>
  <UApp>
    <NuxtPage />
  </UApp>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/i18n/nuxt#locale
\> Learn how to use the \`locale\` prop to change the locale of your app. This also controls the date/time format in components like Calendar, InputDate, and InputTime.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/i18n/vue#locale
\> Learn how to use the \`locale\` prop to change the locale of your app. This also controls the date/time format in components like Calendar, InputDate, and InputTime.

## API

### Props

```ts
/**
 * Props for the App component
 */
interface AppProps {
  tooltip?: TooltipProviderProps | undefined;
  toaster?: ToasterProps | null | undefined;
  locale?: Locale<T> | undefined;
  /**
   * @default "\"body\""
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * The global reading direction of your application. This will be inherited by all primitives.
   */
  dir?: Direction | undefined;
  /**
   * The global scroll body behavior of your application. This will be inherited by the related primitives.
   */
  scrollBody?: boolean | ScrollBodyOption | undefined;
  /**
   * The global `nonce` value of your application. This will be inherited by the related primitives.
   */
  nonce?: string | undefined;
}
```

### Slots

```ts
/**
 * Slots for the App component
 */
interface AppSlots {
  default(): any;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/App.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/app.ts).


# AuthForm

## Usage

Built on top of the [Form](https://ui.nuxt.com/docs/components/form) component, the `AuthForm` component can be used in your pages or wrapped in a [PageCard](https://ui.nuxt.com/docs/components/page-card).

```vue [AuthFormExample.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'

const toast = useToast()

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: () => {
    toast.add({ title: 'Google', description: 'Login with Google' })
  }
}, {
  label: 'GitHub',
  icon: 'i-simple-icons-github',
  onClick: () => {
    toast.add({ title: 'GitHub', description: 'Login with GitHub' })
  }
}]

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>) {
  console.log('Submitted', payload)
}
</script>

<template>
  <div class="flex flex-col items-center justify-center gap-4 p-4">
    <UPageCard class="w-full max-w-md">
      <UAuthForm
        :schema="schema"
        title="Login"
        description="Enter your credentials to access your account."
        icon="i-lucide-user"
        :fields="fields"
        :providers="providers"
        @submit="onSubmit"
      />
    </UPageCard>
  </div>
</template>
```

### Fields

The Form will construct itself based on the `fields` prop and the state will be handled internally.

Use the `fields` prop as an array of objects with the following properties:

- `name: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `type: 'checkbox' | 'select' | 'otp' | 'InputHTMLAttributes['type']'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Each field must include a `type` property, which determines the input component and any additional props applied: `checkbox` fields use [Checkbox](https://ui.nuxt.com/docs/components/checkbox#props) props, `select` fields use [SelectMenu](https://ui.nuxt.com/docs/components/select-menu#props) props, `otp` fields use [PinInput](https://ui.nuxt.com/docs/components/pin-input#props) props, and all other types use [Input](https://ui.nuxt.com/docs/components/input#props) props.

You can also pass any property from the [FormField](https://ui.nuxt.com/docs/components/form-field#props) component to each field.

```vue
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm class="max-w-sm" />
</template>
```

### Title

Use the `title` prop to set the title of the Form.

```vue
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" class="max-w-md" />
</template>
```

### Description

Use the `description` prop to set the description of the Form.

```vue
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." class="max-w-md" />
</template>
```

### Icon

Use the `icon` prop to set the icon of the Form.

```vue
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." icon="i-lucide-user" class="max-w-md" />
</template>
```

### Providers

Use the `providers` prop to add providers to the form.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component such as `variant`, `color`, `to`, etc.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." icon="i-lucide-user" class="max-w-md" />
</template>
```

### Separator

Use the `separator` prop to customize the [Separator](https://ui.nuxt.com/docs/components/separator) between the providers and the fields. Defaults to `or`.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." icon="i-lucide-user" separator="Providers" class="max-w-md" />
</template>
```

You can pass any property from the [Separator](https://ui.nuxt.com/docs/components/separator#props) component to customize it.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." icon="i-lucide-user" class="max-w-md" />
</template>
```

### Submit

Use the `submit` prop to change the submit button of the Form.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component such as `variant`, `color`, `to`, etc.

```vue
<script setup lang="ts">
import type { AuthFormField } from '@nuxt/ui'
</script>

<template>
  <UAuthForm title="Login" description="Enter your credentials to access your account." icon="i-lucide-user" class="max-w-md" />
</template>
```

## Examples

### Within a page

You can wrap the `AuthForm` component with the [PageCard](https://ui.nuxt.com/docs/components/page-card) component to display it within a `login.vue` page for example.

```vue [AuthFormPageExample.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'

const toast = useToast()

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: () => {
    toast.add({ title: 'Google', description: 'Login with Google' })
  }
}, {
  label: 'GitHub',
  icon: 'i-simple-icons-github',
  onClick: () => {
    toast.add({ title: 'GitHub', description: 'Login with GitHub' })
  }
}]

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>) {
  console.log('Submitted', payload)
}
</script>

<template>
  <div class="flex flex-col items-center justify-center gap-4 p-4">
    <UPageCard class="w-full max-w-md">
      <UAuthForm
        :schema="schema"
        :fields="fields"
        :providers="providers"
        title="Welcome back!"
        icon="i-lucide-lock"
        @submit="onSubmit"
      >
        <template #description>
          Don't have an account? <ULink to="#" class="text-primary font-medium">Sign up</ULink>.
        </template>
        <template #password-hint>
          <ULink to="#" class="text-primary font-medium" tabindex="-1">Forgot password?</ULink>
        </template>
        <template #validation>
          <UAlert color="error" icon="i-lucide-info" title="Error signing in" />
        </template>
        <template #footer>
          By signing in, you agree to our <ULink to="#" class="text-primary font-medium">Terms of Service</ULink>.
        </template>
      </UAuthForm>
    </UPageCard>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the AuthForm component
 */
interface AuthFormProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed above the title.
   */
  icon?: any;
  title?: string | undefined;
  description?: string | undefined;
  fields?: F[] | undefined;
  /**
   * Display a list of Button under the description.
   * `{ color: 'neutral', variant: 'subtle', block: true }`{lang="ts-type"}
   */
  providers?: ButtonProps[] | undefined;
  /**
   * The text displayed in the separator.
   * @default "\"or\""
   */
  separator?: string | SeparatorProps | undefined;
  /**
   * Display a submit button at the bottom of the form.
   * `{ label: 'Continue', block: true }`{lang="ts-type"}
   */
  submit?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  schema?: T | undefined;
  validate?: ((state: Partial<InferInput<T>>) => FormError<string>[] | Promise<FormError<string>[]>) | undefined;
  validateOn?: FormInputEvents[] | undefined;
  validateOnInputDelay?: number | undefined;
  disabled?: boolean | undefined;
  loading?: boolean | undefined;
  loadingAuto?: boolean | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; providers?: ClassNameValue; checkbox?: ClassNameValue; select?: ClassNameValue; password?: ClassNameValue; otp?: ClassNameValue; input?: ClassNameValue; separator?: ClassNameValue; form?: ClassNameValue; footer?: ClassNameValue; } | undefined;
  name?: string | undefined;
  autocomplete?: string | undefined;
  acceptcharset?: string | undefined;
  action?: string | undefined;
  enctype?: string | undefined;
  method?: string | undefined;
  novalidate?: Booleanish | undefined;
  target?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attributes
\> This component also supports all native \`\<form>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the AuthForm component
 */
interface AuthFormSlots {
  header(): any;
  leading(): any;
  title(): any;
  description(): any;
  providers(): any;
  validation(): any;
  submit(): any;
  footer(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the AuthForm component
 */
interface AuthFormEmits {
  submit: (payload: [payload: FormSubmitEvent<Reactive<InferInput<T>>>]) => void;
}
```

### Expose

You can access the typed component instance (exposing formRef and state) using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}. For example, in a separate form (e.g. a "reset" form) you can do:

```vue
<script setup lang="ts">
const authForm = useTemplateRef('authForm')
</script>

<template>
  <UAuthForm ref="authForm" />
</template>
```

This gives you access to the following (exposed) properties:

| Name                                                                                                                          | Type                                                                                                                                              |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLFormElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `state`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}   | `Reactive<FormStateType>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}     |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    authForm: {
      slots: {
        root: 'w-full space-y-6',
        header: 'flex flex-col text-center',
        leading: 'mb-2',
        leadingIcon: 'size-8 shrink-0 inline-block',
        title: 'text-xl text-pretty font-semibold text-highlighted',
        description: 'mt-1 text-base text-pretty text-muted',
        body: 'gap-y-6 flex flex-col',
        providers: 'space-y-3',
        checkbox: '',
        select: 'w-full',
        password: 'w-full',
        otp: 'w-full',
        input: 'w-full',
        separator: '',
        form: 'space-y-5',
        footer: 'text-sm text-center text-muted mt-2'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/AuthForm.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/auth-form.ts).


# Avatar

## Usage

The Avatar uses the `<NuxtImg>` component when [`@nuxt/image`](https://github.com/nuxt/image){rel="&#x22;nofollow&#x22;"} is installed, falling back to `img` otherwise.

```vue
<template>
  <UAvatar src="https://github.com/benjamincanac.png" />
</template>
```

\> \[!NOTE]
\> You can pass any property from the HTML \`\<img>\` element such as \`alt\`, \`loading\`, etc.

\> \[!TIP]
\> To opt-out of \`@nuxt/image\`, use the \`as\` prop: \`:as="{ img: 'img' }"\`.

### Src

Use the `src` prop to set the image URL.

```vue
<template>
  <UAvatar src="https://github.com/benjamincanac.png" loading="lazy" />
</template>
```

### Size

Use the `size` prop to set the size of the Avatar.

```vue
<template>
  <UAvatar src="https://github.com/benjamincanac.png" size="xl" loading="lazy" />
</template>
```

\> \[!NOTE]
\> The \`\<img>\` element's \`width\` and \`height\` are automatically set based on the \`size\` prop.

### Icon

Use the `icon` prop to display a fallback [Icon](https://ui.nuxt.com/docs/components/icon).

```vue
<template>
  <UAvatar icon="i-lucide-image" size="md" />
</template>
```

### Text

Use the `text` prop to display a fallback text.

```vue
<template>
  <UAvatar text="+1" size="md" />
</template>
```

### Alt

When no icon or text is provided, the **initials** of the `alt` prop is used as fallback.

```vue
<template>
  <UAvatar alt="Benjamin Canac" size="md" />
</template>
```

\> \[!NOTE]
\> The \`alt\` prop is passed to the \`img\` element as the \`alt\` attribute.

### Chip

Use the `chip` prop to display a chip around the Avatar.

```vue
<template>
  <UAvatar src="https://github.com/benjamincanac.png" loading="lazy" />
</template>
```

## Examples

### With tooltip

You can use a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) component to display a tooltip when hovering the Avatar.

```vue [AvatarTooltipExample.vue]
<template>
  <UTooltip text="Benjamin Canac">
    <UAvatar
      src="https://github.com/benjamincanac.png"
      alt="Benjamin Canac"
      loading="lazy"
    />
  </UTooltip>
</template>
```

### With mask

You can use a CSS mask to display an Avatar with a custom shape instead of a simple circle.

```vue [AvatarMaskExample.vue]
<template>
  <UAvatar class="rounded-none squircle" src="https://avatars.githubusercontent.com/u/739984?v=4" alt="Benjamin Canac" loading="lazy" />
</template>

<style>
.squircle {
  mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 0C20 0 0 20 0 100s20 100 100 100 100-20 100-100S180 0 100 0Z'/%3e%3c/svg%3e");
  mask-size: contain;
  mask-position: center;
  mask-repeat: no-repeat;
}
</style>
```

## API

### Props

```ts
/**
 * Props for the Avatar component
 */
interface AvatarProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  src?: string | undefined;
  alt?: string | undefined;
  icon?: any;
  text?: string | undefined;
  size?: "2xl" | "md" | "3xs" | "2xs" | "xs" | "sm" | "lg" | "xl" | "3xl" | undefined;
  chip?: boolean | ChipProps | undefined;
  ui?: { root?: ClassNameValue; image?: ClassNameValue; fallback?: ClassNameValue; icon?: ClassNameValue; } | undefined;
  loading?: "lazy" | "eager" | undefined;
  referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
  crossorigin?: "" | "anonymous" | "use-credentials" | undefined;
  decoding?: "async" | "auto" | "sync" | undefined;
  height?: Numberish | undefined;
  sizes?: string | undefined;
  srcset?: string | undefined;
  usemap?: string | undefined;
  width?: Numberish | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes
\> This component also supports all native \`\<img>\` HTML attributes.

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    avatar: {
      slots: {
        root: 'inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated',
        image: 'h-full w-full rounded-[inherit] object-cover',
        fallback: 'font-medium leading-none text-muted truncate',
        icon: 'text-muted shrink-0'
      },
      variants: {
        size: {
          '3xs': {
            root: 'size-4 text-[8px]'
          },
          '2xs': {
            root: 'size-5 text-[10px]'
          },
          xs: {
            root: 'size-6 text-xs'
          },
          sm: {
            root: 'size-7 text-sm'
          },
          md: {
            root: 'size-8 text-base'
          },
          lg: {
            root: 'size-9 text-lg'
          },
          xl: {
            root: 'size-10 text-xl'
          },
          '2xl': {
            root: 'size-11 text-[22px]'
          },
          '3xl': {
            root: 'size-12 text-2xl'
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Avatar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/avatar.ts).


# AvatarGroup

## Usage

Wrap multiple [Avatar](https://ui.nuxt.com/docs/components/avatar) within an AvatarGroup to stack them.

```vue
<template>
  <UAvatarGroup>
    <UAvatar src="https://github.com/benjamincanac.png" alt="Benjamin Canac" />
    <UAvatar src="https://github.com/romhml.png" alt="Romain Hamel" />
    <UAvatar src="https://github.com/noook.png" alt="Neil Richter" />
  </UAvatarGroup>
</template>
```

### Size

Use the `size` prop to change the size of all the avatars.

```vue
<template>
  <UAvatarGroup size="xl">
    <UAvatar src="https://github.com/benjamincanac.png" alt="Benjamin Canac" loading="lazy" />
    <UAvatar src="https://github.com/romhml.png" alt="Romain Hamel" loading="lazy" />
    <UAvatar src="https://github.com/noook.png" alt="Neil Richter" loading="lazy" />
  </UAvatarGroup>
</template>
```

### Max

Use the `max` prop to limit the number of avatars displayed. The rest is displayed as an `+X` avatar.

```vue
<template>
  <UAvatarGroup :max="2">
    <UAvatar src="https://github.com/benjamincanac.png" alt="Benjamin Canac" loading="lazy" />
    <UAvatar src="https://github.com/romhml.png" alt="Romain Hamel" loading="lazy" />
    <UAvatar src="https://github.com/noook.png" alt="Neil Richter" loading="lazy" />
  </UAvatarGroup>
</template>
```

## Examples

### With tooltip

Wrap each avatar with a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) to display a tooltip on hover.

```vue [AvatarGroupTooltipExample.vue]
<template>
  <UAvatarGroup>
    <UTooltip text="benjamincanac">
      <UAvatar
        src="https://github.com/benjamincanac.png"
        alt="Benjamin Canac"
        loading="lazy"
      />
    </UTooltip>

    <UTooltip text="romhml">
      <UAvatar
        src="https://github.com/romhml.png"
        alt="Romain Hamel"
        loading="lazy"
      />
    </UTooltip>

    <UTooltip text="noook">
      <UAvatar
        src="https://github.com/noook.png"
        alt="Neil Richter"
        loading="lazy"
      />
    </UTooltip>
  </UAvatarGroup>
</template>
```

### With chip

Wrap each avatar with a [Chip](https://ui.nuxt.com/docs/components/chip) to display a chip around the avatar.

```vue [AvatarGroupChipExample.vue]
<template>
  <UAvatarGroup>
    <UAvatar
      src="https://github.com/benjamincanac.png"
      alt="Benjamin Canac"
      loading="lazy"
      :chip="{ inset: true, color: 'success' }"
    />

    <UAvatar
      src="https://github.com/romhml.png"
      alt="Romain Hamel"
      loading="lazy"
      :chip="{ inset: true, color: 'warning' }"
    />

    <UAvatar
      src="https://github.com/noook.png"
      alt="Neil Richter"
      loading="lazy"
      :chip="{ inset: true, color: 'error' }"
    />
  </UAvatarGroup>
</template>
```

### With link

Wrap each avatar with a [Link](https://ui.nuxt.com/docs/components/link) to make them clickable.

```vue [AvatarGroupLinkExample.vue]
<template>
  <UAvatarGroup>
    <ULink
      to="https://github.com/benjamincanac"
      target="_blank"
      class="hover:ring-primary transition"
      raw
    >
      <UAvatar
        src="https://github.com/benjamincanac.png"
        alt="Benjamin Canac"
        loading="lazy"
      />
    </ULink>

    <ULink
      to="https://github.com/romhml"
      target="_blank"
      class="hover:ring-primary transition"
      raw
    >
      <UAvatar
        src="https://github.com/romhml.png"
        alt="Romain Hamel"
        loading="lazy"
      />
    </ULink>

    <ULink
      to="https://github.com/noook"
      target="_blank"
      class="hover:ring-primary transition"
      raw
    >
      <UAvatar
        src="https://github.com/noook.png"
        alt="Neil Richter"
        loading="lazy"
      />
    </ULink>
  </UAvatarGroup>
</template>
```

### With mask

Wrap an avatar with a CSS mask to display it with a custom shape.

```vue [AvatarGroupMaskExample.vue]
<template>
  <UAvatarGroup :ui="{ base: 'rounded-none squircle' }">
    <UAvatar
      src="https://github.com/benjamincanac.png"
      alt="Benjamin Canac"
      loading="lazy"
      class="rounded-none squircle"
    />

    <UAvatar
      src="https://github.com/romhml.png"
      alt="Romain Hamel"
      loading="lazy"
      class="rounded-none squircle"
    />

    <UAvatar
      src="https://github.com/noook.png"
      alt="Neil Richter"
      loading="lazy"
      class="rounded-none squircle"
    />
  </UAvatarGroup>
</template>

<style>
.squircle {
  mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 0C20 0 0 20 0 100s20 100 100 100 100-20 100-100S180 0 100 0Z'/%3e%3c/svg%3e");
  mask-size: contain;
  mask-position: center;
  mask-repeat: no-repeat;
}
</style>
```

\> \[!WARNING]
\> The \`chip\` prop does not work correctly when using a mask. Chips may be cut depending on the mask shape.

## API

### Props

```ts
/**
 * Props for the AvatarGroup component
 */
interface AvatarGroupProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "2xl" | "md" | "3xs" | "2xs" | "xs" | "sm" | "lg" | "xl" | "3xl" | undefined;
  /**
   * The maximum number of avatars to display.
   */
  max?: string | number | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the AvatarGroup component
 */
interface AvatarGroupSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    avatarGroup: {
      slots: {
        root: 'inline-flex flex-row-reverse justify-end',
        base: 'relative rounded-full ring-bg first:me-0'
      },
      variants: {
        size: {
          '3xs': {
            base: 'ring -me-0.5'
          },
          '2xs': {
            base: 'ring -me-0.5'
          },
          xs: {
            base: 'ring -me-0.5'
          },
          sm: {
            base: 'ring-2 -me-1.5'
          },
          md: {
            base: 'ring-2 -me-1.5'
          },
          lg: {
            base: 'ring-2 -me-1.5'
          },
          xl: {
            base: 'ring-3 -me-2'
          },
          '2xl': {
            base: 'ring-3 -me-2'
          },
          '3xl': {
            base: 'ring-3 -me-2'
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/AvatarGroup.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/avatar-group.ts).


# Badge

## Usage

Use the default slot to set the label of the Badge.

```vue
<template>
  <UBadge>
    Badge
  </UBadge>
</template>
```

### Label

Use the `label` prop to set the label of the Badge.

```vue
<template>
  <UBadge label="Badge" />
</template>
```

### Color

Use the `color` prop to change the color of the Badge.

```vue
<template>
  <UBadge color="neutral">
    Badge
  </UBadge>
</template>
```

### Variant

Use the `variant` props to change the variant of the Badge.

```vue
<template>
  <UBadge color="neutral" variant="outline">
    Badge
  </UBadge>
</template>
```

### Size

Use the `size` prop to change the size of the Badge.

```vue
<template>
  <UBadge size="xl">
    Badge
  </UBadge>
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the Badge.

```vue
<template>
  <UBadge icon="i-lucide-rocket" size="md" color="primary" variant="solid">
    Badge
  </UBadge>
</template>
```

Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.

```vue
<template>
  <UBadge trailing-icon="i-lucide-arrow-right" size="md">
    Badge
  </UBadge>
</template>
```

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the Badge.

```vue
<template>
  <UBadge size="md" color="neutral" variant="outline">
    Badge
  </UBadge>
</template>
```

## Examples

### `class` prop

Use the `class` prop to override the base styles of the Badge.

```vue
<template>
  <UBadge class="font-bold rounded-full">
    Badge
  </UBadge>
</template>
```

## API

### Props

```ts
/**
 * Props for the Badge component
 */
interface BadgeProps {
  /**
   * The element or component this component should render as.
   * @default "\"span\""
   */
  as?: any;
  label?: string | number | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the badge with equal padding on all sides.
   */
  square?: boolean | undefined;
  ui?: { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; } | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
}
```

### Slots

```ts
/**
 * Slots for the Badge component
 */
interface BadgeSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    badge: {
      slots: {
        base: 'font-medium inline-flex items-center',
        label: 'truncate',
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailingIcon: 'shrink-0'
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        size: {
          xs: {
            base: 'text-[8px]/3 px-1 py-0.5 gap-1 rounded-sm',
            leadingIcon: 'size-3',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-3'
          },
          sm: {
            base: 'text-[10px]/3 px-1.5 py-1 gap-1 rounded-sm',
            leadingIcon: 'size-3',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-3'
          },
          md: {
            base: 'text-xs px-2 py-1 gap-1 rounded-md',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          lg: {
            base: 'text-sm px-2 py-1 gap-1.5 rounded-md',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          xl: {
            base: 'text-base px-2.5 py-1 gap-1.5 rounded-md',
            leadingIcon: 'size-6',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-6'
          }
        },
        square: {
          true: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: 'bg-primary text-inverted'
        },
        {
          color: 'secondary',
          variant: 'solid',
          class: 'bg-secondary text-inverted'
        },
        {
          color: 'success',
          variant: 'solid',
          class: 'bg-success text-inverted'
        },
        {
          color: 'info',
          variant: 'solid',
          class: 'bg-info text-inverted'
        },
        {
          color: 'warning',
          variant: 'solid',
          class: 'bg-warning text-inverted'
        },
        {
          color: 'error',
          variant: 'solid',
          class: 'bg-error text-inverted'
        },
        {
          color: 'primary',
          variant: 'outline',
          class: 'text-primary ring ring-inset ring-primary/50'
        },
        {
          color: 'secondary',
          variant: 'outline',
          class: 'text-secondary ring ring-inset ring-secondary/50'
        },
        {
          color: 'success',
          variant: 'outline',
          class: 'text-success ring ring-inset ring-success/50'
        },
        {
          color: 'info',
          variant: 'outline',
          class: 'text-info ring ring-inset ring-info/50'
        },
        {
          color: 'warning',
          variant: 'outline',
          class: 'text-warning ring ring-inset ring-warning/50'
        },
        {
          color: 'error',
          variant: 'outline',
          class: 'text-error ring ring-inset ring-error/50'
        },
        {
          color: 'primary',
          variant: 'soft',
          class: 'bg-primary/10 text-primary'
        },
        {
          color: 'secondary',
          variant: 'soft',
          class: 'bg-secondary/10 text-secondary'
        },
        {
          color: 'success',
          variant: 'soft',
          class: 'bg-success/10 text-success'
        },
        {
          color: 'info',
          variant: 'soft',
          class: 'bg-info/10 text-info'
        },
        {
          color: 'warning',
          variant: 'soft',
          class: 'bg-warning/10 text-warning'
        },
        {
          color: 'error',
          variant: 'soft',
          class: 'bg-error/10 text-error'
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: 'bg-primary/10 text-primary ring ring-inset ring-primary/25'
        },
        {
          color: 'secondary',
          variant: 'subtle',
          class: 'bg-secondary/10 text-secondary ring ring-inset ring-secondary/25'
        },
        {
          color: 'success',
          variant: 'subtle',
          class: 'bg-success/10 text-success ring ring-inset ring-success/25'
        },
        {
          color: 'info',
          variant: 'subtle',
          class: 'bg-info/10 text-info ring ring-inset ring-info/25'
        },
        {
          color: 'warning',
          variant: 'subtle',
          class: 'bg-warning/10 text-warning ring ring-inset ring-warning/25'
        },
        {
          color: 'error',
          variant: 'subtle',
          class: 'bg-error/10 text-error ring ring-inset ring-error/25'
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: 'text-inverted bg-inverted'
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: 'ring ring-inset ring-accented text-default bg-default'
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: 'text-default bg-elevated'
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: 'ring ring-inset ring-accented text-default bg-elevated'
        },
        {
          size: 'xs',
          square: true,
          class: 'p-0.5'
        },
        {
          size: 'sm',
          square: true,
          class: 'p-1'
        },
        {
          size: 'md',
          square: true,
          class: 'p-1'
        },
        {
          size: 'lg',
          square: true,
          class: 'p-1'
        },
        {
          size: 'xl',
          square: true,
          class: 'p-1'
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'solid',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Badge.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/badge.ts).


# Banner

## Usage

### Title

Use the `title` prop to display a title on the Banner.

```vue
<template>
  <UBanner title="This is a banner with an important message." />
</template>
```

### Icon

Use the `icon` prop to display an icon on the Banner.

```vue
<template>
  <UBanner icon="i-lucide-info" title="This is a banner with an icon." />
</template>
```

### Color

Use the `color` prop to change the color of the Banner.

```vue
<template>
  <UBanner color="neutral" icon="i-lucide-info" title="This is a banner with an icon." />
</template>
```

### Close

Use the `close` prop to display a [Button](https://ui.nuxt.com/docs/components/button) to dismiss the Banner. Defaults to `false`.

\> \[!TIP]
\> A \`close\` event will be emitted when the close button is clicked.

```vue [BannerExample.vue]
<script setup lang="ts">
import type { BannerProps } from '@nuxt/ui'

const { id = 'example' } = defineProps<{
  id?: string
  title?: string
  color?: BannerProps['color']
  closeIcon?: string
}>()

function onClose() {
  localStorage.removeItem(`banner-${id}`)

  setTimeout(() => {
    document.querySelector('html')?.classList.remove('hide-banner')
  }, 1000)
}

onBeforeMount(() => {
  localStorage.removeItem(`banner-${id}`)
})
</script>

<template>
  <UBanner
    :id="id"
    :title="title || 'This is a closable banner'"
    :color="color"
    :close-icon="closeIcon"
    close
    @close="onClose"
  />
</template>
```

\> \[!NOTE]
\> When closed, \`banner-${id}\` will be stored in the local storage to prevent it from being displayed again. For the example above, \`banner-example\` will be stored in the local storage.

\> \[!CAUTION]
\> To persist the dismissed state across page reloads, you must specify an \`id\` prop. Without an explicit \`id\`, the banner will only be hidden for the current session and will reappear on page reload.

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue [BannerExample.vue]
<script setup lang="ts">
import type { BannerProps } from '@nuxt/ui'

const { id = 'example' } = defineProps<{
  id?: string
  title?: string
  color?: BannerProps['color']
  closeIcon?: string
}>()

function onClose() {
  localStorage.removeItem(`banner-${id}`)

  setTimeout(() => {
    document.querySelector('html')?.classList.remove('hide-banner')
  }, 1000)
}

onBeforeMount(() => {
  localStorage.removeItem(`banner-${id}`)
})
</script>

<template>
  <UBanner
    :id="id"
    :title="title || 'This is a closable banner'"
    :color="color"
    :close-icon="closeIcon"
    close
    @close="onClose"
  />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Actions

Use the `actions` prop to add some [Button](https://ui.nuxt.com/docs/components/button) actions to the Banner.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UBanner title="This is a banner with actions." />
</template>
```

\> \[!NOTE]
\> The action buttons default to \`color="neutral"\` and \`size="xs"\`. You can customize these values by passing them directly to each action button.

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UBanner to="https://nuxtlabs.com/" target="_blank" title="NuxtLabs is joining Vercel!" color="primary" />
</template>
```

\> \[!NOTE]
\> The \`NuxtLink\` component will inherit all other attributes you pass to the \`User\` component.

## Examples

### Within `app.vue`

Use the Banner component in your `app.vue` or in a layout:

```vue [app.vue] {3}
<template>
  <UApp>
    <UBanner icon="i-lucide-construction" title="Nuxt UI v4 has been released!" />

    <UHeader />

    <UMain>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </UMain>

    <UFooter />
  </UApp>
</template>
```

## API

### Props

```ts
/**
 * Props for the Banner component
 */
interface BannerProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * A unique id saved to local storage to remember if the banner has been dismissed.
   * Without an explicit id, the banner will not be persisted and will reappear on page reload.
   */
  id?: string | undefined;
  /**
   * The icon displayed next to the title.
   */
  icon?: any;
  title?: string | undefined;
  /**
   * Display a list of actions next to the title.
   * `{ color: 'neutral', size: 'xs' }`{lang="ts-type"}
   */
  actions?: ButtonProps[] | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * Display a close button to dismiss the banner.
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; left?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; icon?: ClassNameValue; title?: ClassNameValue; actions?: ClassNameValue; close?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Banner component
 */
interface BannerSlots {
  leading(): any;
  title(): any;
  actions(): any;
  close(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Banner component
 */
interface BannerEmits {
  close: (payload: []) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    banner: {
      slots: {
        root: [
          'relative z-50 w-full',
          'transition-colors'
        ],
        container: 'flex items-center justify-between gap-3 h-12',
        left: 'hidden lg:flex-1 lg:flex lg:items-center',
        center: 'flex items-center gap-1.5 min-w-0',
        right: 'lg:flex-1 flex items-center justify-end',
        icon: 'size-5 shrink-0 text-inverted pointer-events-none',
        title: 'text-sm text-inverted font-medium truncate',
        actions: 'flex gap-1.5 shrink-0 isolate',
        close: 'text-inverted hover:bg-default/10 focus-visible:bg-default/10 -me-1.5 lg:me-0'
      },
      variants: {
        color: {
          primary: {
            root: 'bg-primary'
          },
          secondary: {
            root: 'bg-secondary'
          },
          success: {
            root: 'bg-success'
          },
          info: {
            root: 'bg-info'
          },
          warning: {
            root: 'bg-warning'
          },
          error: {
            root: 'bg-error'
          },
          neutral: {
            root: 'bg-inverted'
          }
        },
        to: {
          true: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          to: true,
          class: {
            root: 'hover:bg-primary/90'
          }
        },
        {
          color: 'secondary',
          to: true,
          class: {
            root: 'hover:bg-secondary/90'
          }
        },
        {
          color: 'success',
          to: true,
          class: {
            root: 'hover:bg-success/90'
          }
        },
        {
          color: 'info',
          to: true,
          class: {
            root: 'hover:bg-info/90'
          }
        },
        {
          color: 'warning',
          to: true,
          class: {
            root: 'hover:bg-warning/90'
          }
        },
        {
          color: 'error',
          to: true,
          class: {
            root: 'hover:bg-error/90'
          }
        },
        {
          color: 'neutral',
          to: true,
          class: {
            root: 'hover:bg-inverted/90'
          }
        }
      ],
      defaultVariants: {
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Banner.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/banner.ts).


# BlogPost

## Usage

The BlogPost component provides a flexible way to display an `<article>` element with customizable content including title, description, image, etc.

```vue
<template>
  <u-blog-post :authors=[{"name":"Anthony Fu","description":"antfu7","avatar":{"src":"https://github.com/antfu.png","loading":"lazy"},"to":"https://github.com/antfu","target":"_blank"}] date=2024-11-25 description=Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects. image=https://nuxt.com/assets/blog/nuxt-icon/cover.png target=_blank title=Introducing Nuxt Icon v1 to=https://nuxt.com/blog/nuxt-icon-v1-0 />
</template>
```

\> \[!TIP]
\> See: /docs/components/blog-posts
\> Use the \`BlogPosts\` component to display multiple blog posts in a responsive grid layout.

### Title

Use the `title` prop to display the title of the BlogPost.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" />
</template>
```

### Description

Use the `description` prop to display the description of the BlogPost.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." />
</template>
```

### Date

Use the `date` prop to display the date of the BlogPost.

\> \[!TIP]
\> The date is automatically formatted to the \[current locale]\(/docs/getting-started/integrations/i18n/nuxt#locale). You can either pass a \`Date\` object or a string.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." date="2024-11-25" />
</template>
```

### Badge

Use the `badge` prop to display a [Badge](https://ui.nuxt.com/docs/components/badge) in the BlogPost.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." badge="Release" />
</template>
```

You can pass any property from the [Badge](https://ui.nuxt.com/docs/components/badge#props) component to customize it.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." />
</template>
```

### Image

Use the `image` prop to display an image in the BlogPost.

\> \[!NOTE]
\> If \[\`@nuxt/image\`]\(https\://image.nuxt.com/get-started/installation) is installed, the \`\<NuxtImg>\` component will be used instead of the native \`img\` tag.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" />
</template>
```

### Authors

Use the `authors` prop to display a list of [User](https://ui.nuxt.com/docs/components/user) in the BlogPost as an array of objects with the following properties:

- `name?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: Omit<AvatarProps, 'size'>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `chip?: boolean | Omit<ChipProps, 'size' | 'inset'>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `size?: UserProps['size']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `orientation?: UserProps['orientation']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { UserProps } from '@nuxt/ui'
</script>

<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" />
</template>
```

When the `authors` prop has more than one item, the [AvatarGroup](https://ui.nuxt.com/docs/components/avatar-group) component is used.

```vue
<script setup lang="ts">
import type { UserProps } from '@nuxt/ui'
</script>

<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" />
</template>
```

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" to="https://nuxt.com/blog/nuxt-icon-v1-0" target="_blank" />
</template>
```

### Variant

Use the `variant` prop to change the style of the BlogPost.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" to="https://nuxt.com/blog/nuxt-icon-v1-0" target="_blank" variant="naked" />
</template>
```

\> \[!NOTE]
\> The styling will be different wether you provide a \`to\` prop or an \`image\`.

### Orientation

Use the `orientation` prop to change the BlogPost orientation. Defaults to `vertical`.

```vue
<template>
  <UBlogPost title="Introducing Nuxt Icon v1" description="Discover Nuxt Icon v1 - a modern, versatile, and customizable icon solution for your Nuxt projects." image="https://nuxt.com/assets/blog/nuxt-icon/cover.png" date="2024-11-25" to="https://nuxt.com/blog/nuxt-icon-v1-0" target="_blank" orientation="horizontal" variant="outline" />
</template>
```

## API

### Props

```ts
/**
 * Props for the BlogPost component
 */
interface BlogPostProps {
  /**
   * The element or component this component should render as.
   * @default "\"article\""
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The date of the blog post. Can be a string or a Date object.
   */
  date?: string | Date | undefined;
  /**
   * Display a badge on the blog post.
   * Can be a string or an object.
   * `{ color: 'neutral', variant: 'subtle' }`{lang="ts-type"}
   */
  badge?: string | BadgeProps | undefined;
  /**
   * The authors of the blog post.
   */
  authors?: UserProps[] | undefined;
  /**
   * The image of the blog post. Can be a string or an object.
   */
  image?: string | (Partial<ImgHTMLAttributes> & { [key: string]: any; }) | undefined;
  /**
   * The orientation of the blog post.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "naked" | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; image?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; authors?: ClassNameValue; avatar?: ClassNameValue; meta?: ClassNameValue; date?: ClassNameValue; badge?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the BlogPost component
 */
interface BlogPostSlots {
  date(): any;
  badge(): any;
  title(): any;
  description(): any;
  authors(): any;
  header(): any;
  body(): any;
  footer(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    blogPost: {
      slots: {
        root: 'relative group/blog-post flex flex-col rounded-lg overflow-hidden',
        header: 'relative overflow-hidden aspect-[16/9] w-full pointer-events-none',
        body: 'min-w-0 flex-1 flex flex-col',
        footer: '',
        image: 'object-cover object-top w-full h-full',
        title: 'text-xl text-pretty font-semibold text-highlighted',
        description: 'mt-1 text-base text-pretty',
        authors: 'pt-4 mt-auto flex flex-wrap gap-x-3 gap-y-1.5',
        avatar: '',
        meta: 'flex items-center gap-2 mb-2',
        date: 'text-sm',
        badge: ''
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'lg:grid lg:grid-cols-2 lg:items-center gap-x-8',
            body: 'justify-center p-4 sm:p-6 lg:px-0'
          },
          vertical: {
            root: 'flex flex-col',
            body: 'p-4 sm:p-6'
          }
        },
        variant: {
          outline: {
            root: 'bg-default ring ring-default',
            date: 'text-toned',
            description: 'text-muted'
          },
          soft: {
            root: 'bg-elevated/50',
            date: 'text-muted',
            description: 'text-toned'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default',
            date: 'text-muted',
            description: 'text-toned'
          },
          ghost: {
            date: 'text-toned',
            description: 'text-muted',
            header: 'shadow-lg rounded-lg'
          },
          naked: {
            root: 'p-0 sm:p-0',
            date: 'text-toned',
            description: 'text-muted',
            header: 'shadow-lg rounded-lg'
          }
        },
        to: {
          true: {
            root: [
              'has-focus-visible:ring-2 has-focus-visible:ring-primary',
              'transition'
            ],
            image: 'transform transition-transform duration-200 group-hover/blog-post:scale-110',
            avatar: 'transform transition-transform duration-200 hover:scale-115 focus-visible:outline-primary'
          }
        },
        image: {
          true: ''
        }
      },
      compoundVariants: [
        {
          variant: 'outline',
          to: true,
          class: {
            root: 'hover:bg-elevated/50'
          }
        },
        {
          variant: 'soft',
          to: true,
          class: {
            root: 'hover:bg-elevated'
          }
        },
        {
          variant: 'subtle',
          to: true,
          class: {
            root: 'hover:bg-elevated hover:ring-accented'
          }
        },
        {
          variant: 'ghost',
          to: true,
          class: {
            root: 'hover:bg-elevated/50',
            header: [
              'group-hover/blog-post:shadow-none',
              'transition-all'
            ]
          }
        },
        {
          variant: 'ghost',
          to: true,
          orientation: 'vertical',
          class: {
            header: 'group-hover/blog-post:rounded-b-none'
          }
        },
        {
          variant: 'ghost',
          to: true,
          orientation: 'horizontal',
          class: {
            header: 'group-hover/blog-post:rounded-r-none'
          }
        },
        {
          orientation: 'vertical',
          image: false,
          variant: 'naked',
          class: {
            body: 'p-0 sm:p-0'
          }
        }
      ],
      defaultVariants: {
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/BlogPost.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/blog-post.ts).


# BlogPosts

## Usage

The BlogPosts component provides a flexible layout to display a list of [BlogPost](https://ui.nuxt.com/docs/components/blog-post) components using either the default slot or the `posts` prop.

```vue {2,8}
<template>
  <UBlogPosts>
    <UBlogPost
      v-for="(post, index) in posts"
      :key="index"
      v-bind="post"
    />
  </UBlogPosts>
</template>
```

### Posts

Use the `posts` prop as an array of objects with the properties of the [BlogPost](https://ui.nuxt.com/docs/components/blog-post#props) component.

```vue
<script setup lang="ts">
import type { BlogPostProps } from '@nuxt/ui'
</script>

<template>
  <UBlogPosts />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the BlogPosts. Defaults to `horizontal`.

```vue
<script setup lang="ts">
import type { BlogPostProps } from '@nuxt/ui'
</script>

<template>
  <UBlogPosts orientation="vertical" />
</template>
```

\> \[!TIP]
\> When using the \`posts\` prop instead of the default slot, the \`orientation\` of the posts is automatically reversed, \`horizontal\` to \`vertical\` and vice versa.

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the BlogPosts component in a page to create a blog page:

```vue [pages/blog/index.vue] {11-18}
<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () => queryCollection('posts').all())
</script>

<template>
  <UPage>
    <UPageHero title="Blog" />

    <UPageBody>
      <UContainer>
        <UBlogPosts>
          <UBlogPost
            v-for="(post, index) in posts"
            :key="index"
            v-bind="post"
            :to="post.path"
          />
        </UBlogPosts>
      </UContainer>
    </UPageBody>
  </UPage>
</template>
```

\> \[!NOTE]
\> In this example, the \`posts\` are fetched using \`queryCollection\` from the \`@nuxt/content\` module.

\> \[!TIP]
\> The \`to\` prop is overridden here since \`@nuxt/content\` uses the \`path\` property.

## API

### Props

```ts
/**
 * Props for the BlogPosts component
 */
interface BlogPostsProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  posts?: BlogPostProps[] | undefined;
  /**
   * The orientation of the blog posts.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the BlogPosts component
 */
interface BlogPostsSlots {
  date(): any;
  badge(): any;
  title(): any;
  description(): any;
  authors(): any;
  header(): any;
  body(): any;
  footer(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    blogPosts: {
      base: 'flex flex-col gap-8 lg:gap-y-16',
      variants: {
        orientation: {
          horizontal: 'sm:grid sm:grid-cols-2 lg:grid-cols-3',
          vertical: ''
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/BlogPosts.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/blog-posts.ts).


# Breadcrumb

## Usage

Use the Breadcrumb component to show the current page's location in your site's hierarchy.

```vue
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'

const items = ref<BreadcrumbItem[]>([
  {
    label: 'Docs',
    icon: 'i-lucide-book-open',
    to: '/docs',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
  },
  {
    label: 'Breadcrumb',
    icon: 'i-lucide-link',
    to: '/docs/components/breadcrumb',
  },
])
</script>

<template>
  <UBreadcrumb :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLabel?: ClassNameValue, separator?: ClassNameValue, separatorIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'

const items = ref<BreadcrumbItem[]>([
  {
    label: 'Docs',
    icon: 'i-lucide-book-open',
    to: '/docs',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
  },
  {
    label: 'Breadcrumb',
    icon: 'i-lucide-link',
    to: '/docs/components/breadcrumb',
  },
])
</script>

<template>
  <UBreadcrumb :items="items" />
</template>
```

\> \[!NOTE]
\> A \`span\` is rendered instead of a link when the \`to\` property is not defined.

### Separator Icon

Use the `separator-icon` prop to customize the [Icon](https://ui.nuxt.com/docs/components/icon) between each item. Defaults to `i-lucide-chevron-right`.

```vue
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'

const items = ref<BreadcrumbItem[]>([
  {
    label: 'Docs',
    icon: 'i-lucide-book-open',
    to: '/docs',
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
  },
  {
    label: 'Breadcrumb',
    icon: 'i-lucide-link',
    to: '/docs/components/breadcrumb',
  },
])
</script>

<template>
  <UBreadcrumb separator-icon="i-lucide-arrow-right" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronRight\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronRight\` key.

## Examples

### With separator slot

Use the `#separator` slot to customize the separator between each item.

```vue [BreadcrumbSeparatorSlotExample.vue]
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'

const items: BreadcrumbItem[] = [
  {
    label: 'Docs',
    to: '/docs'
  },
  {
    label: 'Components',
    to: '/docs/components'
  },
  {
    label: 'Breadcrumb',
    to: '/docs/components/breadcrumb'
  }
]
</script>

<template>
  <UBreadcrumb :items="items">
    <template #separator>
      <span class="mx-2 text-muted">/</span>
    </template>
  </UBreadcrumb>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [BreadcrumbCustomSlotExample.vue]
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'

const items = [
  {
    label: 'Home',
    to: '/'
  },
  {
    slot: 'dropdown' as const,
    icon: 'i-lucide-ellipsis',
    children: [
      {
        label: 'Documentation',
        to: '/docs'
      },
      {
        label: 'Themes'
      },
      {
        label: 'GitHub'
      }
    ]
  },
  {
    label: 'Components',
    to: '/docs/components'
  },
  {
    label: 'Breadcrumb',
    to: '/docs/components/breadcrumb'
  }
] satisfies BreadcrumbItem[]
</script>

<template>
  <UBreadcrumb :items="items">
    <template #dropdown="{ item }">
      <UDropdownMenu :items="item.children">
        <UButton :icon="item.icon" color="neutral" variant="link" class="p-0.5" />
      </UDropdownMenu>
    </template>
  </UBreadcrumb>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can also use the \`#item\`, \`#item-leading\`, \`#item-label\` and \`#item-trailing\` slots to customize all items.

## API

### Props

```ts
/**
 * Props for the Breadcrumb component
 */
interface BreadcrumbProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  items?: T[] | undefined;
  /**
   * The icon to use as a separator.
   */
  separatorIcon?: any;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  ui?: { root?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLeadingAvatar?: ClassNameValue; linkLeadingAvatarSize?: ClassNameValue; linkLabel?: ClassNameValue; separator?: ClassNameValue; separatorIcon?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Breadcrumb component
 */
interface BreadcrumbSlots {
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-trailing(): any;
  separator(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    breadcrumb: {
      slots: {
        root: 'relative min-w-0',
        list: 'flex items-center gap-1.5',
        item: 'flex min-w-0',
        link: 'group relative flex items-center gap-1.5 text-sm min-w-0 focus-visible:outline-primary',
        linkLeadingIcon: 'shrink-0 size-5',
        linkLeadingAvatar: 'shrink-0',
        linkLeadingAvatarSize: '2xs',
        linkLabel: 'truncate',
        separator: 'flex',
        separatorIcon: 'shrink-0 size-5 text-muted'
      },
      variants: {
        active: {
          true: {
            link: 'text-primary font-semibold'
          },
          false: {
            link: 'text-muted font-medium'
          }
        },
        disabled: {
          true: {
            link: 'cursor-not-allowed opacity-75'
          }
        },
        to: {
          true: ''
        }
      },
      compoundVariants: [
        {
          disabled: false,
          active: false,
          to: true,
          class: {
            link: [
              'hover:text-default',
              'transition-colors'
            ]
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Breadcrumb.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/breadcrumb.ts).


# Button

## Usage

Use the default slot to set the label of the Button.

```vue
<template>
  <UButton>
    Button
  </UButton>
</template>
```

### Label

Use the `label` prop to set the label of the Button.

```vue
<template>
  <UButton label="Button" />
</template>
```

### Color

Use the `color` prop to change the color of the Button.

```vue
<template>
  <UButton color="neutral">
    Button
  </UButton>
</template>
```

### Variant

Use the `variant` prop to change the variant of the Button.

```vue
<template>
  <UButton color="neutral" variant="outline">
    Button
  </UButton>
</template>
```

### Size

Use the `size` prop to change the size of the Button.

```vue
<template>
  <UButton size="xl">
    Button
  </UButton>
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the Button.

```vue
<template>
  <UButton icon="i-lucide-rocket" size="md" color="primary" variant="solid">
    Button
  </UButton>
</template>
```

Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.

```vue
<template>
  <UButton trailing-icon="i-lucide-arrow-right" size="md">
    Button
  </UButton>
</template>
```

The `label` as prop or slot is optional so you can use the Button as an icon-only button.

```vue
<template>
  <UButton icon="i-lucide-search" size="md" color="primary" variant="solid" />
</template>
```

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the Button.

```vue
<template>
  <UButton size="md" color="neutral" variant="outline">
    Button
  </UButton>
</template>
```

The `label` as prop or slot is optional so you can use the Button as an avatar-only button.

```vue
<template>
  <UButton size="md" color="neutral" variant="outline" />
</template>
```

### Link

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<template>
  <UButton to="https://github.com/nuxt/ui" target="_blank">
    Button
  </UButton>
</template>
```

When the Button is a link or when using the `active` prop, you can use the `active-color` and `active-variant` props to customize the active state.

```vue
<template>
  <UButton active color="neutral" variant="outline" active-color="primary" active-variant="solid">
    Button
  </UButton>
</template>
```

You can also use the `active-class` and `inactive-class` props to customize the active state.

```vue
<template>
  <UButton active active-class="font-bold" inactive-class="font-light">
    Button
  </UButton>
</template>
```

\> \[!TIP]
\> You can configure these styles globally in your \`app.config.ts\` file under the \`ui.button.variants.active\` key.
\> \`\`\`ts
\> export default defineAppConfig({
\> ui: {
\> button: {
\> variants: {
\> active: {
\> true: {
\> base: 'font-bold'
\> }
\> }
\> }
\> }
\> }
\> })
\>
\> \`\`\`

### Loading

Use the `loading` prop to show a loading icon and disable the Button.

```vue
<template>
  <UButton loading :trailing="false">
    Button
  </UButton>
</template>
```

Use the `loading-auto` prop to show the loading icon automatically while the `@click` promise is pending.

```vue [ButtonLoadingAutoExample.vue]
<script setup lang="ts">
async function onClick() {
  return new Promise<void>(res => setTimeout(res, 1000))
}
</script>

<template>
  <UButton loading-auto @click="onClick">
    Button
  </UButton>
</template>
```

This also works with the [Form](https://ui.nuxt.com/docs/components/form) component.

```vue [ButtonLoadingAutoFormExample.vue]
<script setup lang="ts">
const state = reactive({ fullName: '' })

async function onSubmit() {
  return new Promise<void>(res => setTimeout(res, 1000))
}

async function validate(data: Partial<typeof state>) {
  if (!data.fullName?.length) return [{ name: 'fullName', message: 'Required' }]
  return []
}
</script>

<template>
  <UForm :state="state" :validate="validate" @submit="onSubmit">
    <UFormField name="fullName" label="Full name">
      <UInput v-model="state.fullName" />
    </UFormField>
    <UButton type="submit" class="mt-2" loading-auto>
      Submit
    </UButton>
  </UForm>
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <UButton loading loading-icon="i-lucide-loader">
    Button
  </UButton>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the Button.

```vue
<template>
  <UButton disabled>
    Button
  </UButton>
</template>
```

## Examples

### `class` prop

Use the `class` prop to override the base styles of the Button.

```vue
<template>
  <UButton class="font-bold rounded-full">
    Button
  </UButton>
</template>
```

### `ui` prop

Use the `ui` prop to override the slots styles of the Button.

```vue
<template>
  <UButton icon="i-lucide-rocket" color="neutral" variant="outline">
    Button
  </UButton>
</template>
```

## API

### Props

```ts
/**
 * Props for the Button component
 */
interface ButtonProps {
  label?: string | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  activeColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  activeVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  ui?: { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; } | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * Route Location the link should navigate to when clicked on.
   */
  to?: string | St | vt | undefined;
  /**
   * Class to apply when the link is active
   */
  activeClass?: string | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Value passed to the attribute `aria-current` when the link is exact active.
   */
  ariaCurrentValue?: "page" | "step" | "location" | "date" | "time" | "true" | "false" | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  /**
   * Calls `router.replace` instead of `router.push`.
   */
  replace?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  download?: any;
  /**
   * An alias for `to`. If used with `to`, `href` will be ignored
   */
  href?: string | St | vt | undefined;
  hreflang?: string | undefined;
  media?: string | undefined;
  ping?: string | undefined;
  /**
   * A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
   */
  rel?: "noopener" | "noreferrer" | "nofollow" | "sponsored" | "ugc" | (string & {}) | null | undefined;
  /**
   * Where to display the linked URL, as the name for a browsing context.
   */
  target?: (string & {}) | "_blank" | "_parent" | "_self" | "_top" | null | undefined;
  referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  /**
   * Force the link to be active independent of the current route.
   */
  active?: boolean | undefined;
  /**
   * Will only be active if the current route is an exact match.
   */
  exact?: boolean | undefined;
  /**
   * Allows controlling how the current route query sets the link as active.
   */
  exactQuery?: boolean | "partial" | undefined;
  /**
   * Will only be active if the current route hash is an exact match.
   */
  exactHash?: boolean | undefined;
  /**
   * The class to apply when the link is inactive.
   */
  inactiveClass?: string | undefined;
  /**
   * Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
   */
  external?: boolean | undefined;
  /**
   * If set to true, no rel attribute will be added to the link
   */
  noRel?: boolean | undefined;
  /**
   * A class to apply to links that have been prefetched.
   */
  prefetchedClass?: string | undefined;
  /**
   * When enabled will prefetch middleware, layouts and payloads of links in the viewport.
   */
  prefetch?: boolean | undefined;
  /**
   * Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility.
   */
  prefetchOn?: "visibility" | "interaction" | Partial<{ visibility: boolean; interaction: boolean; }> | undefined;
  /**
   * Escape hatch to disable `prefetch` attribute.
   */
  noPrefetch?: boolean | undefined;
  /**
   * An option to either add or remove trailing slashes in the `href` for this specific link.
   * Overrides the global `trailingSlash` option if provided.
   */
  trailingSlash?: "remove" | "append" | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/src/runtime/components/Link.vue#L13
\> The \`Button\` component extends the \`Link\` component. Check out the source code on GitHub.

### Slots

```ts
/**
 * Slots for the Button component
 */
interface ButtonSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    button: {
      slots: {
        base: [
          'rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75',
          'transition-colors'
        ],
        label: 'truncate',
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailingIcon: 'shrink-0'
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: '',
          ghost: '',
          link: ''
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-xs gap-1',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-xs gap-1.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          md: {
            base: 'px-2.5 py-1.5 text-sm gap-1.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          lg: {
            base: 'px-3 py-2 text-sm gap-2',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6'
          }
        },
        block: {
          true: {
            base: 'w-full justify-center',
            trailingIcon: 'ms-auto'
          }
        },
        square: {
          true: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        active: {
          true: {
            base: ''
          },
          false: {
            base: ''
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: 'text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
        },
        {
          color: 'secondary',
          variant: 'solid',
          class: 'text-inverted bg-secondary hover:bg-secondary/75 active:bg-secondary/75 disabled:bg-secondary aria-disabled:bg-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary'
        },
        {
          color: 'success',
          variant: 'solid',
          class: 'text-inverted bg-success hover:bg-success/75 active:bg-success/75 disabled:bg-success aria-disabled:bg-success focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success'
        },
        {
          color: 'info',
          variant: 'solid',
          class: 'text-inverted bg-info hover:bg-info/75 active:bg-info/75 disabled:bg-info aria-disabled:bg-info focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info'
        },
        {
          color: 'warning',
          variant: 'solid',
          class: 'text-inverted bg-warning hover:bg-warning/75 active:bg-warning/75 disabled:bg-warning aria-disabled:bg-warning focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-warning'
        },
        {
          color: 'error',
          variant: 'solid',
          class: 'text-inverted bg-error hover:bg-error/75 active:bg-error/75 disabled:bg-error aria-disabled:bg-error focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-error'
        },
        {
          color: 'primary',
          variant: 'outline',
          class: 'ring ring-inset ring-primary/50 text-primary hover:bg-primary/10 active:bg-primary/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: 'outline',
          class: 'ring ring-inset ring-secondary/50 text-secondary hover:bg-secondary/10 active:bg-secondary/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: 'outline',
          class: 'ring ring-inset ring-success/50 text-success hover:bg-success/10 active:bg-success/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: 'outline',
          class: 'ring ring-inset ring-info/50 text-info hover:bg-info/10 active:bg-info/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: 'outline',
          class: 'ring ring-inset ring-warning/50 text-warning hover:bg-warning/10 active:bg-warning/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: 'outline',
          class: 'ring ring-inset ring-error/50 text-error hover:bg-error/10 active:bg-error/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-error'
        },
        {
          color: 'primary',
          variant: 'soft',
          class: 'text-primary bg-primary/10 hover:bg-primary/15 active:bg-primary/15 focus:outline-none focus-visible:bg-primary/15 disabled:bg-primary/10 aria-disabled:bg-primary/10'
        },
        {
          color: 'secondary',
          variant: 'soft',
          class: 'text-secondary bg-secondary/10 hover:bg-secondary/15 active:bg-secondary/15 focus:outline-none focus-visible:bg-secondary/15 disabled:bg-secondary/10 aria-disabled:bg-secondary/10'
        },
        {
          color: 'success',
          variant: 'soft',
          class: 'text-success bg-success/10 hover:bg-success/15 active:bg-success/15 focus:outline-none focus-visible:bg-success/15 disabled:bg-success/10 aria-disabled:bg-success/10'
        },
        {
          color: 'info',
          variant: 'soft',
          class: 'text-info bg-info/10 hover:bg-info/15 active:bg-info/15 focus:outline-none focus-visible:bg-info/15 disabled:bg-info/10 aria-disabled:bg-info/10'
        },
        {
          color: 'warning',
          variant: 'soft',
          class: 'text-warning bg-warning/10 hover:bg-warning/15 active:bg-warning/15 focus:outline-none focus-visible:bg-warning/15 disabled:bg-warning/10 aria-disabled:bg-warning/10'
        },
        {
          color: 'error',
          variant: 'soft',
          class: 'text-error bg-error/10 hover:bg-error/15 active:bg-error/15 focus:outline-none focus-visible:bg-error/15 disabled:bg-error/10 aria-disabled:bg-error/10'
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: 'text-primary ring ring-inset ring-primary/25 bg-primary/10 hover:bg-primary/15 active:bg-primary/15 disabled:bg-primary/10 aria-disabled:bg-primary/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: 'subtle',
          class: 'text-secondary ring ring-inset ring-secondary/25 bg-secondary/10 hover:bg-secondary/15 active:bg-secondary/15 disabled:bg-secondary/10 aria-disabled:bg-secondary/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: 'subtle',
          class: 'text-success ring ring-inset ring-success/25 bg-success/10 hover:bg-success/15 active:bg-success/15 disabled:bg-success/10 aria-disabled:bg-success/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: 'subtle',
          class: 'text-info ring ring-inset ring-info/25 bg-info/10 hover:bg-info/15 active:bg-info/15 disabled:bg-info/10 aria-disabled:bg-info/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: 'subtle',
          class: 'text-warning ring ring-inset ring-warning/25 bg-warning/10 hover:bg-warning/15 active:bg-warning/15 disabled:bg-warning/10 aria-disabled:bg-warning/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: 'subtle',
          class: 'text-error ring ring-inset ring-error/25 bg-error/10 hover:bg-error/15 active:bg-error/15 disabled:bg-error/10 aria-disabled:bg-error/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-error'
        },
        {
          color: 'primary',
          variant: 'ghost',
          class: 'text-primary hover:bg-primary/10 active:bg-primary/10 focus:outline-none focus-visible:bg-primary/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'secondary',
          variant: 'ghost',
          class: 'text-secondary hover:bg-secondary/10 active:bg-secondary/10 focus:outline-none focus-visible:bg-secondary/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'success',
          variant: 'ghost',
          class: 'text-success hover:bg-success/10 active:bg-success/10 focus:outline-none focus-visible:bg-success/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'info',
          variant: 'ghost',
          class: 'text-info hover:bg-info/10 active:bg-info/10 focus:outline-none focus-visible:bg-info/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'warning',
          variant: 'ghost',
          class: 'text-warning hover:bg-warning/10 active:bg-warning/10 focus:outline-none focus-visible:bg-warning/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'error',
          variant: 'ghost',
          class: 'text-error hover:bg-error/10 active:bg-error/10 focus:outline-none focus-visible:bg-error/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent'
        },
        {
          color: 'primary',
          variant: 'link',
          class: 'text-primary hover:text-primary/75 active:text-primary/75 disabled:text-primary aria-disabled:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: 'link',
          class: 'text-secondary hover:text-secondary/75 active:text-secondary/75 disabled:text-secondary aria-disabled:text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: 'link',
          class: 'text-success hover:text-success/75 active:text-success/75 disabled:text-success aria-disabled:text-success focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: 'link',
          class: 'text-info hover:text-info/75 active:text-info/75 disabled:text-info aria-disabled:text-info focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: 'link',
          class: 'text-warning hover:text-warning/75 active:text-warning/75 disabled:text-warning aria-disabled:text-warning focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: 'link',
          class: 'text-error hover:text-error/75 active:text-error/75 disabled:text-error aria-disabled:text-error focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: 'text-inverted bg-inverted hover:bg-inverted/90 active:bg-inverted/90 disabled:bg-inverted aria-disabled:bg-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: 'ring ring-inset ring-accented text-default bg-default hover:bg-elevated active:bg-elevated disabled:bg-default aria-disabled:bg-default focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: 'text-default bg-elevated hover:bg-accented/75 active:bg-accented/75 focus:outline-none focus-visible:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated'
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: 'ring ring-inset ring-accented text-default bg-elevated hover:bg-accented/75 active:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          variant: 'ghost',
          class: 'text-default hover:bg-elevated active:bg-elevated focus:outline-none focus-visible:bg-elevated hover:disabled:bg-transparent dark:hover:disabled:bg-transparent hover:aria-disabled:bg-transparent dark:hover:aria-disabled:bg-transparent'
        },
        {
          color: 'neutral',
          variant: 'link',
          class: 'text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted focus:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-inverted'
        },
        {
          size: 'xs',
          square: true,
          class: 'p-1'
        },
        {
          size: 'sm',
          square: true,
          class: 'p-1.5'
        },
        {
          size: 'md',
          square: true,
          class: 'p-1.5'
        },
        {
          size: 'lg',
          square: true,
          class: 'p-2'
        },
        {
          size: 'xl',
          square: true,
          class: 'p-2'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'solid',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Button.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/button.ts).


# Calendar

## Usage

Use the `v-model` directive to control the selected date.

```vue
<script setup lang="ts">
const value = ref(new CalendarDate(2022, 2, 3))
</script>

<template>
  <UCalendar v-model="value" />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UCalendar />
</template>
```

\*\*Nuxt:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The date format is determined by the \`locale\` prop of the App component.
\*\*Vue:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/vue#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The date format is determined by the \`locale\` prop of the App component.

### Multiple

Use the `multiple` prop to allow multiple selections.

```vue
<script setup lang="ts">
const value = ref(new CalendarDate(2022,2,4, 2022,2,6, 2022,2,8))
</script>

<template>
  <UCalendar multiple v-model="value" />
</template>
```

### Range

Use the `range` prop to select a range of dates.

```vue
<template>
  <UCalendar range v-model="value" />
</template>
```

### Color

Use the `color` prop to change the color of the calendar.

```vue
<template>
  <UCalendar color="neutral" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the calendar.

```vue
<template>
  <UCalendar variant="subtle" />
</template>
```

### Size

Use the `size` prop to change the size of the calendar.

```vue
<template>
  <UCalendar size="xl" />
</template>
```

### Disabled

Use the `disabled` prop to disable the calendar.

```vue
<template>
  <UCalendar disabled />
</template>
```

### Number Of Months

Use the `numberOfMonths` prop to change the number of months in the calendar.

```vue
<template>
  <UCalendar :number-of-months="3" />
</template>
```

### Month Controls

Use the `month-controls` prop to show the month controls. Defaults to `true`.

```vue
<template>
  <UCalendar :month-controls="false" />
</template>
```

### Year Controls

Use the `year-controls` prop to show the year controls. Defaults to `true`.

```vue
<template>
  <UCalendar :year-controls="false" />
</template>
```

### Fixed Weeks

Use the `fixed-weeks` prop to display the calendar with fixed weeks.

```vue
<template>
  <UCalendar :fixed-weeks="false" />
</template>
```

### Week Numbers `4.4+`

Use the `week-numbers` prop to display week numbers in the calendar.

```vue
<template>
  <UCalendar week-numbers fixed-weeks />
</template>
```

## Examples

### With chip events

Use the [Chip](https://ui.nuxt.com/docs/components/chip) component to add events to specific days.

```vue [CalendarEventsExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))

function getColorByDate(date: Date) {
  const isWeekend = date.getDay() % 6 == 0
  const isDayMeeting = date.getDay() % 3 == 0

  if (isWeekend) {
    return undefined
  }

  if (isDayMeeting) {
    return 'error'
  }

  return 'success'
}
</script>

<template>
  <UCalendar v-model="modelValue">
    <template #day="{ day }">
      <UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
        {{ day.day }}
      </UChip>
    </template>
  </UCalendar>
</template>
```

### With disabled dates

Use the `is-date-disabled` prop with a function to mark specific dates as disabled.

```vue [CalendarDisabledDatesExample.vue]
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateDisabled = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>
```

### With unavailable dates

Use the `is-date-unavailable` prop with a function to mark specific dates as unavailable.

```vue [CalendarUnavailableDatesExample.vue]
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateUnavailable = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
```

### With min/max dates

Use the `min-value` and `max-value` props to limit the dates.

```vue [CalendarMinMaxDatesExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>

<template>
  <UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>
```

### With other calendar systems

You can use other calenders from `@internationalized/date` to implement a different calendar system.

```vue [CalendarOtherSystemExample.vue]
<script lang="ts" setup>
import { CalendarDate, HebrewCalendar } from '@internationalized/date'

const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1))
</script>

<template>
  <UCalendar v-model="hebrewDate" />
</template>
```

\> \[!NOTE]
\> See: https\://react-spectrum.adobe.com/internationalized/date/Calendar.html#implementations
\> You can check all the available calendars on \`@internationalized/date\` docs.

### With external controls

You can control the calendar with external controls by manipulating the date passed in the `v-model`.

```vue [CalendarExternalControlsExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" :month-controls="false" :year-controls="false" />

    <div class="flex justify-between gap-4">
      <UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
        Prev
      </UButton>

      <UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
        Next
      </UButton>
    </div>
  </div>
</template>
```

### As a date picker

Use a [Button](https://ui.nuxt.com/docs/components/button) and a [Popover](https://ui.nuxt.com/docs/components/popover) component to create a date picker.

```vue [CalendarDatePickerExample.vue]
<script setup lang="ts">
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" />
    </template>
  </UPopover>
</template>
```

### As a date range picker

Use a [Button](https://ui.nuxt.com/docs/components/button) and a [Popover](https://ui.nuxt.com/docs/components/popover) component to create a date range picker.

```vue [CalendarDateRangePickerExample.vue]
<script setup lang="ts">
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 20),
  end: new CalendarDate(2022, 2, 10)
})
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      <template v-if="modelValue.start">
        <template v-if="modelValue.end">
          {{ df.format(modelValue.start.toDate(getLocalTimeZone())) }} - {{ df.format(modelValue.end.toDate(getLocalTimeZone())) }}
        </template>

        <template v-else>
          {{ df.format(modelValue.start.toDate(getLocalTimeZone())) }}
        </template>
      </template>
      <template v-else>
        Pick a date
      </template>
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" :number-of-months="2" range />
    </template>
  </UPopover>
</template>
```

## API

### Props

```ts
/**
 * Props for the Calendar component
 */
interface CalendarProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon to use for the next year control.
   */
  nextYearIcon?: any;
  /**
   * Configure the next year button.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  nextYear?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon to use for the next month control.
   */
  nextMonthIcon?: any;
  /**
   * Configure the next month button.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  nextMonth?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon to use for the previous year control.
   */
  prevYearIcon?: any;
  /**
   * Configure the prev year button.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  prevYear?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon to use for the previous month control.
   */
  prevMonthIcon?: any;
  /**
   * Configure the prev month button.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  prevMonth?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * Whether or not a range of dates can be selected
   */
  range?: R | undefined;
  /**
   * Whether or not multiple dates can be selected
   */
  multiple?: M | undefined;
  /**
   * Show month controls
   * @default "true"
   */
  monthControls?: boolean | undefined;
  /**
   * Show year controls
   * @default "true"
   */
  yearControls?: boolean | undefined;
  defaultValue?: CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[];
  modelValue?: null | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[];
  weekNumbers?: boolean | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; heading?: ClassNameValue; grid?: ClassNameValue; gridRow?: ClassNameValue; gridWeekDaysRow?: ClassNameValue; gridBody?: ClassNameValue; headCell?: ClassNameValue; headCellWeek?: ClassNameValue; cell?: ClassNameValue; cellTrigger?: ClassNameValue; cellWeek?: ClassNameValue; } | undefined;
  defaultPlaceholder?: CalendarDate | CalendarDateTime | ZonedDateTime;
  placeholder?: CalendarDate | CalendarDateTime | ZonedDateTime;
  /**
   * When combined with `isDateUnavailable`, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected.
   */
  allowNonContiguousRanges?: boolean | undefined;
  /**
   * This property causes the previous and next buttons to navigate by the number of months displayed at once, rather than one month
   */
  pagedNavigation?: boolean | undefined;
  /**
   * Whether or not to prevent the user from deselecting a date without selecting another date first
   */
  preventDeselect?: boolean | undefined;
  /**
   * The maximum number of days that can be selected in a range
   */
  maximumDays?: number | undefined;
  /**
   * The day of the week to start the calendar on
   */
  weekStartsOn?: WeekStartsOn | undefined;
  /**
   * The format to use for the weekday strings provided via the weekdays slot prop
   */
  weekdayFormat?: WeekDayFormat | undefined;
  /**
   * Whether or not to always display 6 weeks in the calendar
   * @default "true"
   */
  fixedWeeks?: boolean | undefined;
  maxValue?: CalendarDate | CalendarDateTime | ZonedDateTime;
  minValue?: CalendarDate | CalendarDateTime | ZonedDateTime;
  /**
   * The number of months to display at once
   */
  numberOfMonths?: number | undefined;
  /**
   * Whether or not the calendar is disabled
   */
  disabled?: boolean | undefined;
  /**
   * Whether or not the calendar is readonly
   */
  readonly?: boolean | undefined;
  /**
   * If true, the calendar will focus the selected day, today, or the first day of the month depending on what is visible when the calendar is mounted
   */
  initialFocus?: boolean | undefined;
  /**
   * A function that returns whether or not a date is disabled
   */
  isDateDisabled?: Matcher | undefined;
  /**
   * A function that returns whether or not a date is unavailable
   */
  isDateUnavailable?: Matcher | undefined;
  /**
   * A function that returns whether or not a date is hightable
   */
  isDateHighlightable?: Matcher | undefined;
  /**
   * A function that returns the next page of the calendar. It receives the current placeholder as an argument inside the component.
   */
  nextPage?: ((placeholder: DateValue) => DateValue) | undefined;
  /**
   * A function that returns the previous page of the calendar. It receives the current placeholder as an argument inside the component.
   */
  prevPage?: ((placeholder: DateValue) => DateValue) | undefined;
  /**
   * Whether or not to disable days outside the current view.
   */
  disableDaysOutsideCurrentView?: boolean | undefined;
  /**
   * Which part of the range should be fixed
   */
  fixedDate?: "start" | "end" | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Calendar component
 */
interface CalendarSlots {
  heading(): any;
  day(): any;
  week-day(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Calendar component
 */
interface CalendarEmits {
  update:modelValue: (payload: [date: CalendarModelValue<R, M>]) => void;
  update:placeholder: (payload: [date: DateValue] & [date: DateValue]) => void;
  update:validModelValue: (payload: [date: DateRange]) => void;
  update:startValue: (payload: [date: DateValue | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    calendar: {
      slots: {
        root: '',
        header: 'flex items-center justify-between',
        body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
        heading: 'text-center font-medium truncate mx-auto',
        grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
        gridRow: 'grid grid-cols-7 place-items-center',
        gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
        gridBody: 'grid',
        headCell: 'rounded-md',
        headCellWeek: 'rounded-md text-muted',
        cell: 'relative text-center',
        cellTrigger: [
          'm-0.5 relative flex items-center justify-center rounded-full whitespace-nowrap focus-visible:ring-2 focus:outline-none data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold data-[outside-view]:text-muted',
          'transition'
        ],
        cellWeek: 'relative text-center text-muted'
      },
      variants: {
        color: {
          primary: {
            headCell: 'text-primary',
            cellTrigger: 'focus-visible:ring-primary'
          },
          secondary: {
            headCell: 'text-secondary',
            cellTrigger: 'focus-visible:ring-secondary'
          },
          success: {
            headCell: 'text-success',
            cellTrigger: 'focus-visible:ring-success'
          },
          info: {
            headCell: 'text-info',
            cellTrigger: 'focus-visible:ring-info'
          },
          warning: {
            headCell: 'text-warning',
            cellTrigger: 'focus-visible:ring-warning'
          },
          error: {
            headCell: 'text-error',
            cellTrigger: 'focus-visible:ring-error'
          },
          neutral: {
            headCell: 'text-highlighted',
            cellTrigger: 'focus-visible:ring-inverted'
          }
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        size: {
          xs: {
            heading: 'text-xs',
            cell: 'text-xs',
            cellWeek: 'text-xs',
            headCell: 'text-[10px]',
            headCellWeek: 'text-[10px]',
            cellTrigger: 'size-7',
            body: 'space-y-2 pt-2'
          },
          sm: {
            heading: 'text-xs',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-xs',
            cellTrigger: 'size-7'
          },
          md: {
            heading: 'text-sm',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-sm',
            cellTrigger: 'size-8'
          },
          lg: {
            heading: 'text-md',
            headCell: 'text-md',
            headCellWeek: 'text-md',
            cellTrigger: 'size-9 text-md'
          },
          xl: {
            heading: 'text-lg',
            headCell: 'text-lg',
            headCellWeek: 'text-lg',
            cellTrigger: 'size-10 text-lg'
          }
        },
        weekNumbers: {
          true: {
            gridRow: 'grid-cols-8',
            gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-primary data-[selected]:text-inverted data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
          }
        },
        {
          color: 'secondary',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-secondary data-[selected]:text-inverted data-today:not-data-[selected]:text-secondary data-[highlighted]:bg-secondary/20 hover:not-data-[selected]:bg-secondary/20'
          }
        },
        {
          color: 'success',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-success data-[selected]:text-inverted data-today:not-data-[selected]:text-success data-[highlighted]:bg-success/20 hover:not-data-[selected]:bg-success/20'
          }
        },
        {
          color: 'info',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-info data-[selected]:text-inverted data-today:not-data-[selected]:text-info data-[highlighted]:bg-info/20 hover:not-data-[selected]:bg-info/20'
          }
        },
        {
          color: 'warning',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-warning data-[selected]:text-inverted data-today:not-data-[selected]:text-warning data-[highlighted]:bg-warning/20 hover:not-data-[selected]:bg-warning/20'
          }
        },
        {
          color: 'error',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-error data-[selected]:text-inverted data-today:not-data-[selected]:text-error data-[highlighted]:bg-error/20 hover:not-data-[selected]:bg-error/20'
          }
        },
        {
          color: 'primary',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/50 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/10 hover:not-data-[selected]:bg-primary/10'
          }
        },
        {
          color: 'secondary',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-secondary/50 data-[selected]:text-secondary data-today:not-data-[selected]:text-secondary data-[highlighted]:bg-secondary/10 hover:not-data-[selected]:bg-secondary/10'
          }
        },
        {
          color: 'success',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-success/50 data-[selected]:text-success data-today:not-data-[selected]:text-success data-[highlighted]:bg-success/10 hover:not-data-[selected]:bg-success/10'
          }
        },
        {
          color: 'info',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-info/50 data-[selected]:text-info data-today:not-data-[selected]:text-info data-[highlighted]:bg-info/10 hover:not-data-[selected]:bg-info/10'
          }
        },
        {
          color: 'warning',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-warning/50 data-[selected]:text-warning data-today:not-data-[selected]:text-warning data-[highlighted]:bg-warning/10 hover:not-data-[selected]:bg-warning/10'
          }
        },
        {
          color: 'error',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-error/50 data-[selected]:text-error data-today:not-data-[selected]:text-error data-[highlighted]:bg-error/10 hover:not-data-[selected]:bg-error/10'
          }
        },
        {
          color: 'primary',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
          }
        },
        {
          color: 'secondary',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-secondary/10 data-[selected]:text-secondary data-today:not-data-[selected]:text-secondary data-[highlighted]:bg-secondary/20 hover:not-data-[selected]:bg-secondary/20'
          }
        },
        {
          color: 'success',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-success/10 data-[selected]:text-success data-today:not-data-[selected]:text-success data-[highlighted]:bg-success/20 hover:not-data-[selected]:bg-success/20'
          }
        },
        {
          color: 'info',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-info/10 data-[selected]:text-info data-today:not-data-[selected]:text-info data-[highlighted]:bg-info/20 hover:not-data-[selected]:bg-info/20'
          }
        },
        {
          color: 'warning',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-warning/10 data-[selected]:text-warning data-today:not-data-[selected]:text-warning data-[highlighted]:bg-warning/20 hover:not-data-[selected]:bg-warning/20'
          }
        },
        {
          color: 'error',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-error/10 data-[selected]:text-error data-today:not-data-[selected]:text-error data-[highlighted]:bg-error/20 hover:not-data-[selected]:bg-error/20'
          }
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/25 data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
          }
        },
        {
          color: 'secondary',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-secondary/10 data-[selected]:text-secondary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-secondary/25 data-today:not-data-[selected]:text-secondary data-[highlighted]:bg-secondary/20 hover:not-data-[selected]:bg-secondary/20'
          }
        },
        {
          color: 'success',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-success/10 data-[selected]:text-success data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-success/25 data-today:not-data-[selected]:text-success data-[highlighted]:bg-success/20 hover:not-data-[selected]:bg-success/20'
          }
        },
        {
          color: 'info',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-info/10 data-[selected]:text-info data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-info/25 data-today:not-data-[selected]:text-info data-[highlighted]:bg-info/20 hover:not-data-[selected]:bg-info/20'
          }
        },
        {
          color: 'warning',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-warning/10 data-[selected]:text-warning data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-warning/25 data-today:not-data-[selected]:text-warning data-[highlighted]:bg-warning/20 hover:not-data-[selected]:bg-warning/20'
          }
        },
        {
          color: 'error',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-error/10 data-[selected]:text-error data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-error/25 data-today:not-data-[selected]:text-error data-[highlighted]:bg-error/20 hover:not-data-[selected]:bg-error/20'
          }
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: {
            cellTrigger: 'data-[selected]:bg-inverted data-[selected]:text-inverted data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: {
            cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-[selected]:text-default data-[selected]:bg-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/10 hover:not-data-[selected]:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: {
            cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'solid'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Calendar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/calendar.ts).


# Card

## Usage

Use the `header`, `default` and `footer` slots to add content to the Card.

```vue [CardExample.vue]
<template>
  <UCard>
    <template #header>
      <Placeholder class="h-8" />
    </template>

    <Placeholder class="h-32" />

    <template #footer>
      <Placeholder class="h-8" />
    </template>
  </UCard>
</template>
```

### Variant

Use the `variant` prop to change the variant of the Card.

```vue
<template>
  <UCard variant="subtle">
    <Placeholder class="h-32" />
  
    <template #header>
      <Placeholder class="h-8" />
    </template>
    <template #footer>
      <Placeholder class="h-8" />
    </template></UCard>
</template>
```

## API

### Props

```ts
/**
 * Props for the Card component
 */
interface CardProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  variant?: "solid" | "outline" | "soft" | "subtle" | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Card component
 */
interface CardSlots {
  header(): any;
  default(): any;
  footer(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    card: {
      slots: {
        root: 'rounded-lg overflow-hidden',
        header: 'p-4 sm:px-6',
        body: 'p-4 sm:p-6',
        footer: 'p-4 sm:px-6'
      },
      variants: {
        variant: {
          solid: {
            root: 'bg-inverted text-inverted'
          },
          outline: {
            root: 'bg-default ring ring-default divide-y divide-default'
          },
          soft: {
            root: 'bg-elevated/50 divide-y divide-default'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default divide-y divide-default'
          }
        }
      },
      defaultVariants: {
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Card.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/card.ts).


# Carousel

## Usage

Use the Carousel component to display a list of items in a carousel.

```vue [CarouselExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    loop
    arrows
    :autoplay="{ delay: 2000 }"
    wheel-gestures
    :prev="{ variant: 'solid' }"
    :next="{ variant: 'solid' }"
    :items="items"
    :ui="{
      item: 'basis-1/3 ps-0',
      prev: 'sm:start-8',
      next: 'sm:end-8',
      container: 'ms-0'
    }"
  >
    <img :src="item" width="320" height="320">
  </UCarousel>
</template>
```

\> \[!NOTE]
\> Use your mouse to drag the carousel horizontally on desktop.

### Items

Use the `items` prop as an array and render each item using the default slot:

```vue [CarouselItemsExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel v-slot="{ item }" :items="items" class="w-full max-w-xs mx-auto">
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

You can also pass an array of objects with the following properties:

- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can control how many items are visible by using the [`basis`](https://tailwindcss.com/docs/flex-basis){rel="&#x22;nofollow&#x22;"} / [`width`](https://tailwindcss.com/docs/width){rel="&#x22;nofollow&#x22;"} utility classes on the `item`:

```vue [CarouselItemsMultipleExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/468/468?random=1',
  'https://picsum.photos/468/468?random=2',
  'https://picsum.photos/468/468?random=3',
  'https://picsum.photos/468/468?random=4',
  'https://picsum.photos/468/468?random=5',
  'https://picsum.photos/468/468?random=6'
]
</script>

<template>
  <UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-1/3' }">
    <img :src="item" width="234" height="234" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Progress. Defaults to `horizontal`.

\> \[!NOTE]
\> Use your mouse to drag the carousel vertically on desktop.

```vue [CarouselOrientationExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    orientation="vertical"
    :items="items"
    :ui="{ container: 'h-[336px]' }"
    class="w-full max-w-xs mx-auto"
  >
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\> \[!CAUTION]
\> You need to specify a \`height\` on the container in vertical orientation.

### Arrows

Use the `arrows` prop to display prev and next buttons.

```vue [CarouselArrowsExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel v-slot="{ item }" arrows :items="items" class="w-full max-w-xs mx-auto">
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

### Prev / Next

Use the `prev` and `next` props to customize the prev and next buttons with any [Button](https://ui.nuxt.com/docs/components/button) props.

```vue [CarouselPrevNextExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    arrows
    :prev="{ color: 'primary' }"
    :next="{ variant: 'solid' }"
    :items="items"
    class="w-full max-w-xs mx-auto"
  >
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

### Prev / Next Icons

Use the `prev-icon` and `next-icon` props to customize the buttons [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-arrow-left` / `i-lucide-arrow-right`.

```vue [CarouselPrevNextIconExample.vue]
<script setup lang="ts">
defineProps<{
  prevIcon?: string
  nextIcon?: string
}>()

const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    arrows
    :prev-icon="prevIcon"
    :next-icon="nextIcon"
    :items="items"
    class="w-full max-w-xs mx-auto"
  >
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize these icons globally in your \`app.config.ts\` under \`ui.icons.arrowLeft\` / \`ui.icons.arrowRight\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize these icons globally in your \`vite.config.ts\` under \`ui.icons.arrowLeft\` / \`ui.icons.arrowRight\` key.

### Dots

Use the `dots` prop to display a list of dots to scroll to a specific slide.

```vue [CarouselDotsExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel v-slot="{ item }" dots :items="items" class="w-full max-w-xs mx-auto">
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

The number of dots is based on the number of slides displayed in the view:

```vue [CarouselDotsMultipleExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel v-slot="{ item }" dots :items="items" :ui="{ item: 'basis-1/3' }">
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

## Plugins

The Carousel component implements the official [Embla Carousel plugins](https://www.embla-carousel.com/plugins/){rel="&#x22;nofollow&#x22;"}.

### Autoplay

This plugin is used to extend Embla Carousel with **autoplay** functionality.

Use the `autoplay` prop as a boolean or an object to configure the [Autoplay plugin](https://www.embla-carousel.com/plugins/autoplay/){rel="&#x22;nofollow&#x22;"}.

```vue [CarouselAutoplayExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/468/468?random=1',
  'https://picsum.photos/468/468?random=2',
  'https://picsum.photos/468/468?random=3',
  'https://picsum.photos/468/468?random=4',
  'https://picsum.photos/468/468?random=5',
  'https://picsum.photos/468/468?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    loop
    arrows
    dots
    :autoplay="{ delay: 2000 }"
    :items="items"
    :ui="{ item: 'basis-1/3' }"
  >
    <img :src="item" width="234" height="234" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\> \[!NOTE]
\> In this example, we're using the \`loop\` prop for an infinite carousel.

### Auto Scroll

This plugin is used to extend Embla Carousel with **auto scroll** functionality.

Use the `auto-scroll` prop as a boolean or an object to configure the [Auto Scroll plugin](https://www.embla-carousel.com/plugins/auto-scroll/){rel="&#x22;nofollow&#x22;"}.

```vue [CarouselAutoScrollExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/468/468?random=1',
  'https://picsum.photos/468/468?random=2',
  'https://picsum.photos/468/468?random=3',
  'https://picsum.photos/468/468?random=4',
  'https://picsum.photos/468/468?random=5',
  'https://picsum.photos/468/468?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    loop
    dots
    arrows
    auto-scroll
    :items="items"
    :ui="{ item: 'basis-1/3' }"
  >
    <img :src="item" width="234" height="234" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\> \[!NOTE]
\> In this example, we're using the \`loop\` prop for an infinite carousel.

### Auto Height

This plugin is used to extend Embla Carousel with **auto height** functionality. It changes the height of the carousel container to fit the height of the highest slide in view.

Use the `auto-height` prop as a boolean or an object to configure the [Auto Height plugin](https://www.embla-carousel.com/plugins/auto-height/){rel="&#x22;nofollow&#x22;"}.

```vue [CarouselAutoHeightExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/320?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/320?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/320?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    auto-height
    arrows
    dots
    :items="items"
    :ui="{
      container: 'transition-[height]',
      controls: 'absolute -top-8 inset-x-12',
      dots: '-top-7',
      dot: 'w-6 h-1'
    }"
    class="w-full max-w-xs mx-auto"
  >
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\> \[!NOTE]
\> In this example, we add the \`transition-\[height]\` class on the container to animate the height change.

### Class Names

Class Names is a **class name toggle** utility plugin for Embla Carousel that enables you to automate the toggling of class names on your carousel.

Use the `class-names` prop as a boolean or an object to configure the [Class Names plugin](https://www.embla-carousel.com/plugins/class-names/){rel="&#x22;nofollow&#x22;"}.

```vue [CarouselClassNamesExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/528/528?random=1',
  'https://picsum.photos/528/528?random=2',
  'https://picsum.photos/528/528?random=3',
  'https://picsum.photos/528/528?random=4',
  'https://picsum.photos/528/528?random=5',
  'https://picsum.photos/528/528?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    class-names
    arrows
    :items="items"
    :ui="{
      item: 'basis-[70%] transition-opacity [&:not(.is-snapped)]:opacity-10'
    }"
    class="mx-auto max-w-sm"
  >
    <img :src="item" width="264" height="264" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

\> \[!NOTE]
\> In this example, we add the \`transition-opacity \[&\:not(.is-snapped)]\:opacity-10\` classes on the \`item\` to animate the opacity change.

### Fade

This plugin is used to replace the Embla Carousel scroll functionality with **fade transitions**.

Use the `fade` prop as a boolean or an object to configure the [Fade plugin](https://www.embla-carousel.com/plugins/fade/){rel="&#x22;nofollow&#x22;"}.

```vue [CarouselFadeExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    fade
    arrows
    dots
    :items="items"
    class="w-full max-w-xs mx-auto"
  >
    <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

### Wheel Gestures

This plugin is used to extend Embla Carousel with the ability to **use the mouse/trackpad wheel** to navigate the carousel.

Use the `wheel-gestures` prop as a boolean or an object to configure the [Wheel Gestures plugin](https://www.embla-carousel.com/plugins/wheel-gestures/){rel="&#x22;nofollow&#x22;"}.

\> \[!NOTE]
\> Use your mouse wheel to scroll the carousel.

```vue [CarouselWheelGesturesExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/468/468?random=1',
  'https://picsum.photos/468/468?random=2',
  'https://picsum.photos/468/468?random=3',
  'https://picsum.photos/468/468?random=4',
  'https://picsum.photos/468/468?random=5',
  'https://picsum.photos/468/468?random=6'
]
</script>

<template>
  <UCarousel
    v-slot="{ item }"
    loop
    wheel-gestures
    :items="items"
    :ui="{ item: 'basis-1/3' }"
  >
    <img :src="item" width="234" height="234" class="rounded-lg" loading="lazy">
  </UCarousel>
</template>
```

## Examples

### With thumbnails

You can use the [`emblaApi`](https://ui.nuxt.com/#expose) function [scrollTo](https://www.embla-carousel.com/api/methods/#scrollto){rel="&#x22;nofollow&#x22;"} to display thumbnails under the carousel that allows you to navigate to a specific slide.

```vue [CarouselThumbnailsExample.vue]
<script setup lang="ts">
const items = [
  'https://picsum.photos/640/640?random=1',
  'https://picsum.photos/640/640?random=2',
  'https://picsum.photos/640/640?random=3',
  'https://picsum.photos/640/640?random=4',
  'https://picsum.photos/640/640?random=5',
  'https://picsum.photos/640/640?random=6'
]

const carousel = useTemplateRef('carousel')
const activeIndex = ref(0)

function onClickPrev() {
  activeIndex.value--
}
function onClickNext() {
  activeIndex.value++
}
function onSelect(index: number) {
  activeIndex.value = index
}

function select(index: number) {
  activeIndex.value = index

  carousel.value?.emblaApi?.scrollTo(index)
}
</script>

<template>
  <div class="flex-1 w-full">
    <UCarousel
      ref="carousel"
      v-slot="{ item }"
      arrows
      :items="items"
      :prev="{ onClick: onClickPrev }"
      :next="{ onClick: onClickNext }"
      class="w-full max-w-xs mx-auto"
      @select="onSelect"
    >
      <img :src="item" width="320" height="320" class="rounded-lg" loading="lazy">
    </UCarousel>

    <div class="flex gap-1 justify-between pt-4 max-w-xs mx-auto">
      <div
        v-for="(item, index) in items"
        :key="index"
        class="size-11 opacity-25 hover:opacity-100 transition-opacity"
        :class="{ 'opacity-100': activeIndex === index }"
        @click="select(index)"
      >
        <img :src="item" width="44" height="44" class="rounded-lg" loading="lazy">
      </div>
    </div>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the Carousel component
 */
interface CarouselProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Configure the prev button when arrows are enabled.
   */
  prev?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the prev button.
   */
  prevIcon?: any;
  /**
   * Configure the next button when arrows are enabled.
   */
  next?: Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the next button.
   */
  nextIcon?: any;
  /**
   * Display prev and next buttons to scroll the carousel.
   * @default "false"
   */
  arrows?: boolean | undefined;
  /**
   * Display dots to scroll to a specific slide.
   * @default "false"
   */
  dots?: boolean | undefined;
  /**
   * The orientation of the carousel.
   * @default "\"horizontal\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  items?: T[] | undefined;
  /**
   * Enable Autoplay plugin
   * @default "false"
   */
  autoplay?: boolean | Partial<CreateOptionsType<OptionsType>> | undefined;
  /**
   * Enable Auto Scroll plugin
   * @default "false"
   */
  autoScroll?: boolean | Partial<CreateOptionsType<OptionsType>> | undefined;
  /**
   * Enable Auto Height plugin
   * @default "false"
   */
  autoHeight?: boolean | Partial<CreateOptionsType<{ active: boolean; breakpoints: { [key: string]: Omit<Partial<any>, "breakpoints">; }; }>> | undefined;
  /**
   * Enable Class Names plugin
   * @default "false"
   */
  classNames?: boolean | Partial<CreateOptionsType<OptionsType>> | undefined;
  /**
   * Enable Fade plugin
   * @default "false"
   */
  fade?: boolean | Partial<CreateOptionsType<{ active: boolean; breakpoints: { [key: string]: Omit<Partial<any>, "breakpoints">; }; }>> | undefined;
  /**
   * Enable Wheel Gestures plugin
   * @default "false"
   */
  wheelGestures?: boolean | WheelGesturesPluginOptions | undefined;
  ui?: { root?: ClassNameValue; viewport?: ClassNameValue; container?: ClassNameValue; item?: ClassNameValue; controls?: ClassNameValue; arrows?: ClassNameValue; prev?: ClassNameValue; next?: ClassNameValue; dots?: ClassNameValue; dot?: ClassNameValue; } | undefined;
  /**
   * @default "\"center\""
   */
  align?: AlignmentOptionType | undefined;
  /**
   * @default "\"trimSnaps\""
   */
  containScroll?: ScrollContainOptionType | undefined;
  /**
   * @default "1"
   */
  slidesToScroll?: SlidesToScrollOptionType | undefined;
  /**
   * @default "false"
   */
  dragFree?: boolean | undefined;
  /**
   * @default "10"
   */
  dragThreshold?: number | undefined;
  /**
   * @default "0"
   */
  inViewThreshold?: number | number[] | undefined;
  /**
   * @default "false"
   */
  loop?: boolean | undefined;
  /**
   * @default "false"
   */
  skipSnaps?: boolean | undefined;
  /**
   * @default "25"
   */
  duration?: number | undefined;
  /**
   * @default "0"
   */
  startIndex?: number | undefined;
  /**
   * @default "true"
   */
  watchDrag?: DragHandlerOptionType | undefined;
  /**
   * @default "true"
   */
  watchResize?: ResizeHandlerOptionType | undefined;
  /**
   * @default "true"
   */
  watchSlides?: SlidesHandlerOptionType | undefined;
  /**
   * @default "true"
   */
  watchFocus?: FocusHandlerOptionType | undefined;
  /**
   * @default "true"
   */
  active?: boolean | undefined;
  /**
   * @default "{}"
   */
  breakpoints?: { [key: string]: Omit<Partial<CreateOptionsType<{ align: AlignmentOptionType; axis: AxisOptionType; container: string | HTMLElement | null; slides: string | HTMLElement[] | NodeListOf<HTMLElement> | null; containScroll: ScrollContainOptionType; direction: AxisDirectionOptionType; slidesToScroll: SlidesToScrollOptionType; dragFree: boolean; dragThreshold: number; inViewThreshold: number | number[] | undefined; loop: boolean; skipSnaps: boolean; duration: number; startIndex: number; watchDrag: DragHandlerOptionType; watchResize: ResizeHandlerOptionType; watchSlides: SlidesHandlerOptionType; watchFocus: FocusHandlerOptionType; }>>, "breakpoints">; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Carousel component
 */
interface CarouselSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Carousel component
 */
interface CarouselEmits {
  select: (payload: [selectedIndex: number]) => void;
}
```

### Expose

You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
const carousel = useTemplateRef('carousel')
</script>

<template>
  <UCarousel ref="carousel" />
</template>
```

This will give you access to the following:

| Name                                                                                                                           | Type                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emblaRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                             |
| `emblaApi`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | [`Ref<EmblaCarouselType | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}](https://www.embla-carousel.com/api/methods/#typescript){rel="&#x22;nofollow&#x22;"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    carousel: {
      slots: {
        root: 'relative focus:outline-none',
        viewport: 'overflow-hidden',
        container: 'flex items-start',
        item: 'min-w-0 shrink-0 basis-full',
        controls: '',
        arrows: '',
        prev: 'absolute rounded-full',
        next: 'absolute rounded-full',
        dots: 'absolute inset-x-0 -bottom-7 flex flex-wrap items-center justify-center gap-3',
        dot: [
          'cursor-pointer size-3 bg-accented rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-primary',
          'transition'
        ]
      },
      variants: {
        orientation: {
          vertical: {
            container: 'flex-col -mt-4',
            item: 'pt-4',
            prev: 'top-4 sm:-top-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90',
            next: 'bottom-4 sm:-bottom-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90'
          },
          horizontal: {
            container: 'flex-row -ms-4',
            item: 'ps-4',
            prev: 'start-4 sm:-start-12 top-1/2 -translate-y-1/2',
            next: 'end-4 sm:-end-12 top-1/2 -translate-y-1/2'
          }
        },
        active: {
          true: {
            dot: 'data-[state=active]:bg-inverted'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Carousel.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/carousel.ts).


# ChangelogVersion

## Usage

The ChangelogVersion component provides a flexible way to display an `<article>` element with customizable content including title, description, image, etc.

```vue
<template>
  <u-changelog-version :authors=[{"name":"Benjamin Canac","description":"@benjamincanac","avatar":{"src":"https://github.com/benjamincanac.png","loading":"lazy"},"to":"https://x.com/benjamincanac","target":"_blank"},{"name":"Sebastien Chopin","description":"@atinux","avatar":{"src":"https://github.com/atinux.png","loading":"lazy"},"to":"https://x.com/atinux","target":"_blank"},{"name":"Hugo Richard","description":"@hugorcd__","avatar":{"src":"https://github.com/hugorcd.png","loading":"lazy"},"to":"https://x.com/hugorcd__","target":"_blank"}] :ui={"container":"max-w-lg"} date=2025-03-12 description=Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility. image=https://nuxt.com/assets/blog/nuxt-ui-v3.png target=_blank title=Introducing Nuxt UI v3 to=https://nuxt.com/blog/nuxt-ui-v3 />
</template>
```

\> \[!TIP]
\> See: /docs/components/changelog-versions
\> Use the \`ChangelogVersions\` component to display multiple changelog versions in a timeline with an indicator bar on the left.

### Title

Use the `title` prop to display the title of the ChangelogVersion.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" />
</template>
```

### Description

Use the `description` prop to display the description of the ChangelogVersion.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." />
</template>
```

### Date

Use the `date` prop to display the date of the ChangelogVersion.

\> \[!TIP]
\> The date is automatically formatted to the \[current locale]\(/docs/getting-started/integrations/i18n/nuxt#locale). You can either pass a \`Date\` object or a string.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" />
</template>
```

### Badge

Use the `badge` prop to display a [Badge](https://ui.nuxt.com/docs/components/badge) on the ChangelogVersion.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" badge="Release" />
</template>
```

You can pass any property from the [Badge](https://ui.nuxt.com/docs/components/badge#props) component to customize it.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" />
</template>
```

### Image

Use the `image` prop to display an image in the BlogPost.

\> \[!NOTE]
\> If \[\`@nuxt/image\`]\(https\://image.nuxt.com/get-started/installation) is installed, the \`\<NuxtImg>\` component will be used instead of the native \`img\` tag.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" image="https://nuxt.com/assets/blog/nuxt-ui-v3.png" />
</template>
```

### Authors

Use the `authors` prop to display a list of [User](https://ui.nuxt.com/docs/components/user) in the ChangelogVersion as an array of objects with the following properties:

- `name?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: Omit<AvatarProps, 'size'>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `chip?: boolean | Omit<ChipProps, 'size' | 'inset'>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `size?: UserProps['size']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `orientation?: UserProps['orientation']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { UserProps } from '@nuxt/ui'
</script>

<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" image="https://nuxt.com/assets/blog/nuxt-ui-v3.png" />
</template>
```

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" image="https://nuxt.com/assets/blog/nuxt-ui-v3.png" to="https://nuxt.com/blog/nuxt-ui-v3" target="_blank" />
</template>
```

### Indicator

Use the `indicator` prop to hide the indicator dot on the left. Defaults to `true`.

```vue
<template>
  <UChangelogVersion title="Introducing Nuxt UI v3" description="Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility." date="2025-03-12" image="https://nuxt.com/assets/blog/nuxt-ui-v3.png" :indicator="false" />
</template>
```

\> \[!NOTE]
\> When the \`indicator\` prop is \`false\`, the date will be displayed over the title.

## Examples

### With body slot

You can use the `body` slot to display custom content between the image and the authors with:

- the [MDC](https://github.com/nuxt-modules/mdc?tab=readme-ov-file#mdc){rel="&#x22;nofollow&#x22;"} component from `@nuxtjs/mdc` to display some markdown.
- the [ContentRenderer](https://content.nuxt.com/docs/components/content-renderer){rel="&#x22;nofollow&#x22;"} component from `@nuxt/content` to render the content of the page or list.
- or use the `:u-changelog-version` component directly in your content with markdown inside the `body` slot as Nuxt UI provides pre-styled prose components.

```vue [ChangelogVersionMarkdownExample.vue]
<script setup lang="ts">
const content = `
![Nuxt UI v3](https://nuxt.com/assets/blog/nuxt-ui-v3.png)

We are thrilled to introduce Nuxt UI v3, a comprehensive redesign of our UI library that delivers significant improvements in accessibility, performance, and developer experience. This major update represents over 1,500 commits of dedicated work, collaboration, and innovation from our team and the community.

Read the blog post announcement: https://nuxt.com/blog/nuxt-ui-v3

**[Get started with Nuxt UI v3 →](https://ui3.nuxt.com/getting-started/installation/nuxt)**

### 🧩 Reka UI: A New Foundation

We've transitioned from [Headless UI](https://headlessui.com/) to [Reka UI](https://reka-ui.com/) as our core component foundation, bringing:

- **Expanded Component Library**: Access to 55+ primitives, significantly expanding our component offerings
- **Future-Proof Development**: Benefit from Reka UI's growing popularity and continuous improvements
- **First-Class Accessibility**: Built-in accessibility features aligned with our commitment to inclusive design

### 🚀 Tailwind CSS Integration

Nuxt UI now leverages the latest [Tailwind CSS](https://tailwindcss.com), delivering:

- **Exceptional Performance**: Full builds up to 5× faster, with incremental builds over 100× faster
- **Streamlined Toolchain**: Built-in import handling, vendor prefixing, and syntax transforms with zero additional tooling
- **CSS-First Configuration**: Customize and extend the framework directly in CSS instead of JavaScript configuration

### 🎨 Tailwind Variants

We've adopted [Tailwind Variants](https://www.tailwind-variants.org/) to power our design system, offering:

- **Dynamic Styling**: Create flexible component variants with a powerful, intuitive API
- **Type Safety**: Full TypeScript support with intelligent auto-completion
- **Smart Conflict Resolution**: Efficiently merge conflicting styles with predictable results

## Migration from v2

We want to be transparent: migrating from Nuxt UI v2 to v3 requires significant effort. While we've maintained core concepts and components, Nuxt UI v3 has been rebuilt from the ground up to provide enhanced capabilities.

To upgrade your project:

1. Read our detailed [migration guide](https://ui3.nuxt.com/getting-started/migration)
2. Review the new documentation and components before attempting to upgrade
3. Report any issues on our [GitHub repository](https://github.com/nuxt/ui/issues)

## 🙏 Acknowledgements

This release represents thousands of hours of work from our team and the community. We'd like to thank everyone who contributed to making Nuxt UI v3 a reality, especially @romhml, @sandros94, and @hywax for their tremendous work.
`

const version = {
  title: 'Introducing Nuxt UI v3',
  description: 'Nuxt UI v3 is out! After 1500+ commits, this major redesign brings improved accessibility, Tailwind CSS support, and full Vue compatibility.',
  date: '2025-03-12T00:00:00.000Z',
  badge: 'Release',
  to: 'https://nuxt.com/blog/nuxt-ui-v3',
  target: '_blank',
  content,
  authors: [{
    name: 'Benjamin Canac',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'Benjamin Canac',
      loading: 'lazy' as const
    },
    to: 'https://github.com/benjamincanac',
    target: '_blank'
  }]
}
</script>

<template>
  <UChangelogVersion v-bind="version" :ui="{ container: 'max-w-lg' }" class="w-full">
    <template #body>
      <MDC :value="version.content" />
    </template>
  </UChangelogVersion>
</template>
```

## API

### Props

```ts
/**
 * Props for the ChangelogVersion component
 */
interface ChangelogVersionProps {
  /**
   * The element or component this component should render as.
   * @default "\"article\""
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The date of the changelog version. Can be a string or a Date object.
   */
  date?: string | Date | undefined;
  /**
   * Display a badge on the changelog version.
   * Can be a string or an object.
   * `{ color: 'neutral', variant: 'solid' }`{lang="ts-type"}
   */
  badge?: string | BadgeProps | undefined;
  /**
   * The authors of the changelog version.
   */
  authors?: UserProps[] | undefined;
  /**
   * The image of the changelog version. Can be a string or an object.
   */
  image?: string | (Partial<ImgHTMLAttributes> & { [key: string]: any; }) | undefined;
  /**
   * Display an indicator dot on the left.
   * @default "true"
   */
  indicator?: boolean | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; header?: ClassNameValue; meta?: ClassNameValue; date?: ClassNameValue; badge?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; imageWrapper?: ClassNameValue; image?: ClassNameValue; authors?: ClassNameValue; footer?: ClassNameValue; indicator?: ClassNameValue; dot?: ClassNameValue; dotInner?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChangelogVersion component
 */
interface ChangelogVersionSlots {
  header(): any;
  badge(): any;
  date(): any;
  title(): any;
  description(): any;
  image(): any;
  body(): any;
  footer(): any;
  authors(): any;
  actions(): any;
  indicator(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    changelogVersion: {
      slots: {
        root: 'relative',
        container: 'flex flex-col mx-auto max-w-2xl',
        header: '',
        meta: 'flex items-center gap-3 mb-2',
        date: 'text-sm/6 text-toned truncate',
        badge: '',
        title: 'relative text-xl text-pretty font-semibold text-highlighted',
        description: 'text-base text-pretty text-muted mt-1',
        imageWrapper: 'relative overflow-hidden rounded-lg aspect-[16/9] mt-5 group/changelog-version-image',
        image: 'object-cover object-top w-full h-full',
        authors: 'flex flex-wrap gap-x-4 gap-y-1.5',
        footer: 'border-t border-default pt-5 flex items-center justify-between',
        indicator: 'absolute start-0 top-0 w-32 hidden lg:flex items-center justify-end gap-3 min-w-0',
        dot: 'size-4 rounded-full bg-default ring ring-default flex items-center justify-center my-1',
        dotInner: 'size-2 rounded-full bg-primary'
      },
      variants: {
        body: {
          false: {
            footer: 'mt-5'
          }
        },
        badge: {
          false: {
            meta: 'lg:hidden'
          }
        },
        to: {
          true: {
            title: [
              'has-focus-visible:ring-2 has-focus-visible:ring-primary rounded-xs',
              'transition'
            ],
            image: 'transform transition-transform duration-200 group-hover/changelog-version-image:scale-105 group-has-focus-visible/changelog-version-image:scale-105'
          }
        },
        hidden: {
          true: {
            date: 'lg:hidden'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChangelogVersion.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/changelog-version.ts).


# ChangelogVersions

## Usage

The ChangelogVersions component provides a flexible layout to display a list of [ChangelogVersion](https://ui.nuxt.com/docs/components/changelog-version) components using either the default slot or the `versions` prop.

```vue {2,8}
<template>
  <UChangelogVersions>
    <UChangelogVersion
      v-for="(version, index) in versions"
      :key="index"
      v-bind="version"
    />
  </UChangelogVersions>
</template>
```

### Versions

Use the `versions` prop as an array of objects with the properties of the [ChangelogVersion](https://ui.nuxt.com/docs/components/changelog-version#props) component.

```vue
<script setup lang="ts">
import type { ChangelogVersionProps } from '@nuxt/ui'
</script>

<template>
  <UChangelogVersions />
</template>
```

### Indicator

Use the `indicator` prop to hide the indicator bar on the left. Defaults to `true`.

```vue
<script setup lang="ts">
import type { ChangelogVersionProps } from '@nuxt/ui'
</script>

<template>
  <UChangelogVersions :indicator="false" />
</template>
```

### Indicator Motion

Use the `indicator-motion` prop to customize or hide the motion effect on the indicator bar. Defaults to `true` with `{ damping: 30, restDelta: 0.001 }` [spring transition options](https://motion.dev/docs/vue-transitions#spring){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
import type { ChangelogVersionProps } from '@nuxt/ui'
</script>

<template>
  <UChangelogVersions indicator-motion />
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the ChangelogVersions component in a page to create a changelog page:

```vue [pages/changelog.vue] {10-17}
<script setup lang="ts">
const { data: versions } = await useAsyncData('versions', () => queryCollection('versions').all())
</script>

<template>
  <UPage>
    <UPageHero title="Changelog" />

    <UPageBody>
      <UChangelogVersions>
        <UChangelogVersion
          v-for="(version, index) in versions"
          :key="index"
          v-bind="version"
          :to="version.path"
        />
      </UChangelogVersions>
    </UPageBody>
  </UPage>
</template>
```

\> \[!NOTE]
\> In this example, the \`versions\` are fetched using \`queryCollection\` from the \`@nuxt/content\` module.

\> \[!TIP]
\> The \`to\` prop is overridden here since \`@nuxt/content\` uses the \`path\` property.

### With sticky indicator

You can use the `ui` prop and the different slots to make the indicators sticky:

```vue [ChangelogVersionsStickyExample.vue]
<script setup lang="ts">
const versions = [{
  title: 'Nuxt 3.17',
  description: 'Nuxt 3.17 is out - bringing a major reworking of the async data layer, a new built-in component, better warnings, and performance improvements!',
  date: '2025-04-27T00:00:00.000Z',
  image: 'https://nuxt.com/assets/blog/v3.17.png',
  badge: 'v3.17.0',
  to: 'https://nuxt.com/blog/nuxt-3-17',
  target: '_blank',
  authors: [{
    name: 'Daniel Roe',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'Daniel Roe',
      loading: 'lazy' as const
    },
    to: 'https://github.com/danielroe',
    target: '_blank'
  }]
}, {
  title: 'Nuxt 3.16',
  description: 'Nuxt 3.16 is out - packed with features and performance improvements!',
  date: '2024-03-07T00:00:00.000Z',
  image: 'https://nuxt.com/assets/blog/v3.16.png',
  badge: 'v3.16.0',
  to: 'https://nuxt.com/blog/v3-16',
  target: '_blank',
  authors: [{
    name: 'Daniel Roe',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'Daniel Roe',
      loading: 'lazy' as const
    },
    to: 'https://github.com/danielroe',
    target: '_blank'
  }]
}, {
  title: 'Nuxt 3.15',
  description: 'Nuxt 3.15 is out - with Vite 6, better HMR and faster performance!',
  date: '2024-12-24T00:00:00.000Z',
  image: 'https://nuxt.com/assets/blog/v3.15.png',
  badge: 'v3.15.0',
  to: 'https://nuxt.com/blog/v3-15',
  target: '_blank',
  authors: [{
    name: 'Daniel Roe',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'Daniel Roe',
      loading: 'lazy' as const
    },
    to: 'https://github.com/danielroe',
    target: '_blank'
  }]
}]
</script>

<template>
  <UChangelogVersions :indicator="false">
    <UChangelogVersion
      v-for="version in versions"
      :key="version.title"
      v-bind="version"
      :badge="undefined"
      class="flex items-start"
      :ui="{
        container: 'max-w-lg me-0',
        indicator: 'sticky top-(--ui-header-height) pt-4 -mt-4 flex flex-col items-end'
      }"
    >
      <template #indicator>
        <UBadge :label="version.badge" variant="soft" />

        <span class="text-sm text-muted">{{ new Date(version.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) }}</span>
      </template>
    </UChangelogVersion>
  </UChangelogVersions>
</template>
```

### With scroll container `4.4+`

Pass an object to the `indicator` prop to configure the scroll container. By default, the indicator tracks the window/page scroll (<https://motion.dev/docs/vue-use-scroll#page-scroll>{rel="&#x22;nofollow&#x22;"}).

```vue
<script setup lang="ts">
const scrollContainer = ref<HTMLElement>()
</script>

<template>
  <div ref="scrollContainer" class="max-h-96 overflow-y-auto">
    <UChangelogVersions v-if="scrollContainer" :indicator="{ container: scrollContainer }" />
  </div>
</template>
```

\> \[!WARNING]
\> When using a custom \`container\`, make sure the container element is mounted before \`UChangelogVersions\`.

## API

### Props

```ts
/**
 * Props for the ChangelogVersions component
 */
interface ChangelogVersionsProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  versions?: T[] | undefined;
  /**
   * Display an indicator bar on the left.
   * By default, the indicator will track the scroll of the page. (https://motion.dev/docs/vue-use-scroll#page-scroll)
   * @default "true"
   */
  indicator?: boolean | UseScrollOptions | undefined;
  /**
   * Enable scrolling motion effect on the indicator bar.
   * `{ damping: 30, restDelta: 0.001 }`{lang="ts-type"}
   * @default "true"
   */
  indicatorMotion?: boolean | SpringOptions | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; indicator?: ClassNameValue; beam?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChangelogVersions component
 */
interface ChangelogVersionsSlots {
  header(): any;
  badge(): any;
  date(): any;
  title(): any;
  description(): any;
  image(): any;
  body(): any;
  footer(): any;
  authors(): any;
  actions(): any;
  indicator(): any;
  default(): any;
}
```

\> \[!TIP]
\> You can use all the slots of the \[\`ChangelogVersion\`]\(/docs/components/changelog-version#slots) component inside ChangelogVersions, they are automatically forwarded allowing you to customize individual versions when using the \`versions\` prop.
\> \`\`\`vue
\> \<template>
\> \<UChangelogVersions \:versions="versions">
\> \<template #body="{ version }">
\> \<MDC v-if="version.content" \:value="version.content" />
\> \</template>
\> \</UChangelogVersions>
\> \</template>
\>
\> \`\`\`

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    changelogVersions: {
      slots: {
        root: 'relative',
        container: 'flex flex-col gap-y-8 sm:gap-y-12 lg:gap-y-16',
        indicator: 'absolute hidden lg:block overflow-hidden inset-y-3 start-32 h-full w-px bg-border -ms-[8.5px]',
        beam: 'absolute start-0 top-0 w-full bg-primary will-change-[height]'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChangelogVersions.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/changelog-versions.ts).


# Chat

Nuxt UI provides a set of components designed to build AI-powered chat interfaces. They integrate seamlessly with the [Vercel AI SDK](https://ai-sdk.dev/){rel="&#x22;nofollow&#x22;"} for streaming responses, reasoning, tool calling, and more.

\> \[!NOTE]
\> Check out the \[\`Nuxt\`]\(https\://github.com/nuxt-ui-templates/chat) and \[\`Vue\`]\(https\://github.com/nuxt-ui-templates/chat-vue) AI Chat templates on GitHub for production-ready implementations.

## Components

| Component                                                                  | Description                                                     |
| -------------------------------------------------------------------------- | --------------------------------------------------------------- |
| [ChatMessages](https://ui.nuxt.com/docs/components/chat-messages)          | Scrollable message list with auto-scroll and loading indicator. |
| [ChatMessage](https://ui.nuxt.com/docs/components/chat-message)            | Individual message bubble with avatar, actions, and slots.      |
| [ChatPrompt](https://ui.nuxt.com/docs/components/chat-prompt)              | Enhanced textarea for submitting prompts.                       |
| [ChatPromptSubmit](https://ui.nuxt.com/docs/components/chat-prompt-submit) | Submit button with automatic status handling.                   |
| [ChatReasoning](https://ui.nuxt.com/docs/components/chat-reasoning)        | Collapsible block for AI reasoning / thinking process.          |
| [ChatTool](https://ui.nuxt.com/docs/components/chat-tool)                  | Collapsible block for AI tool invocation status.                |
| [ChatShimmer](https://ui.nuxt.com/docs/components/chat-shimmer)            | Text shimmer animation for streaming states.                    |
| [ChatPalette](https://ui.nuxt.com/docs/components/chat-palette)            | Layout wrapper for embedding chat in modals or drawers.         |

## Installation

The Chat components are designed to be used with the [Vercel AI SDK](https://ai-sdk.dev/){rel="&#x22;nofollow&#x22;"}, specifically the [`Chat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat){rel="&#x22;nofollow&#x22;"} class for managing chat state and streaming responses.

Install the required dependencies:

\`\`\`bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue
\`\`\`
\`\`\`bash
yarn add ai @ai-sdk/gateway @ai-sdk/vue
\`\`\`
\`\`\`bash
npm install ai @ai-sdk/gateway @ai-sdk/vue
\`\`\`
\`\`\`bash
bun add ai @ai-sdk/gateway @ai-sdk/vue
\`\`\`

## Server Setup

Create a server API endpoint to handle chat requests using [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text){rel="&#x22;nofollow&#x22;"}. You can use the [Vercel AI Gateway](https://vercel.com/ai-gateway){rel="&#x22;nofollow&#x22;"} to access AI models through a centralized endpoint:

```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  return streamText({
    model: gateway('anthropic/claude-sonnet-4.6'),
    maxOutputTokens: 10000,
    system: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages)
  }).toUIMessageStreamResponse()
})
```

### Reasoning

To enable [reasoning](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#reasoning){rel="&#x22;nofollow&#x22;"}, configure `providerOptions` for your provider ([Anthropic](https://ai-sdk.dev/docs/guides/providers/anthropic#reasoning){rel="&#x22;nofollow&#x22;"}, [Google](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#thinking){rel="&#x22;nofollow&#x22;"}, [OpenAI](https://ai-sdk.dev/docs/guides/providers/openai#reasoning){rel="&#x22;nofollow&#x22;"}):

```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages } from 'ai'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  return streamText({
    model: gateway('anthropic/claude-sonnet-4.6'),
    maxOutputTokens: 10000,
    system: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages),
    providerOptions: {
      anthropic: {
        thinking: {
          type: 'adaptive'
        },
        effort: 'low'
      },
      google: {
        thinkingConfig: {
          includeThoughts: true,
          thinkingLevel: 'low'
        }
      },
      openai: {
        reasoningEffort: 'low',
        reasoningSummary: 'detailed'
      }
    }
  }).toUIMessageStreamResponse()
})
```

### Web Search

Some providers offer built-in web search tools: [Anthropic](https://ai-sdk.dev/docs/guides/providers/anthropic#web-search-tool){rel="&#x22;nofollow&#x22;"}, [Google](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#google-search){rel="&#x22;nofollow&#x22;"}, [OpenAI](https://ai-sdk.dev/providers/ai-sdk-providers/openai#web-search-tool){rel="&#x22;nofollow&#x22;"}.

\`\`\`ts
import { streamText, convertToModelMessages } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('anthropic/claude-sonnet-4.6'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
tools: {
web\_search: anthropic.tools.webSearch\_20250305({})
}
}).toUIMessageStreamResponse()
})
\`\`\`
\`\`\`ts
import { streamText, convertToModelMessages } from 'ai'
import { google } from '@ai-sdk/google'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('google/gemini-3-flash'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
tools: {
google\_search: google.tools.googleSearch({})
}
}).toUIMessageStreamResponse()
})
\`\`\`
\`\`\`ts
import { streamText, convertToModelMessages } from 'ai'
import { openai } from '@ai-sdk/openai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
return streamText({
model: gateway('openai/gpt-5-nano'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
tools: {
web\_search: openai.tools.webSearch({})
}
}).toUIMessageStreamResponse()
})
\`\`\`

### Tool Calling (MCP)

You can enhance your chatbot with tool calling capabilities using the [Model Context Protocol](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools){rel="&#x22;nofollow&#x22;"} (`@ai-sdk/mcp`). This allows the AI to search your documentation or perform other actions:

```ts [server/api/chat.post.ts]
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { streamText, convertToModelMessages, stepCountIs } from 'ai'
import { experimental_createMCPClient } from '@ai-sdk/mcp'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const httpTransport = new StreamableHTTPClientTransport(
    new URL('https://your-app.com/mcp')
  )
  const httpClient = await experimental_createMCPClient({
    transport: httpTransport
  })
  const tools = await httpClient.tools()

  return streamText({
    model: gateway('anthropic/claude-sonnet-4.6'),
    maxOutputTokens: 10000,
    system: 'You are a helpful assistant. Use your tools to search for relevant information before answering questions.',
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(6),
    tools,
    onFinish: async () => {
      await httpClient.close()
    },
    onError: async (error) => {
      console.error(error)
      await httpClient.close()
    }
  }).toUIMessageStreamResponse()
})
```

## Client Setup

Use the `Chat` class from `@ai-sdk/vue` to manage chat state and connect to your server endpoint:

```vue
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'

const input = ref('')

const chat = new Chat({
  onError(error) {
    console.error(error)
  }
})

function onSubmit() {
  chat.sendMessage({ text: input.value })

  input.value = ''
}
</script>

<template>
  <UChatMessages
    :messages="chat.messages"
    :status="chat.status"
  >
    <template #content="{ message }">
      <template
        v-for="(part, index) in message.parts"
        :key="`${message.id}-${part.type}-${index}`"
      >
        <UChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isReasoningStreaming(message, index, chat)"
        >
          <MDC
            :value="part.text"
            :cache-key="`reasoning-${message.id}-${index}`"
            class="*:first:mt-0 *:last:mb-0"
          />
        </UChatReasoning>

        <UChatTool
          v-else-if="isToolUIPart(part)"
          :text="getToolName(part)"
          :streaming="isToolStreaming(part)"
        />

        <MDC
          v-else-if="isTextUIPart(part)"
          :value="part.text"
          :cache-key="`${message.id}-${index}`"
          class="*:first:mt-0 *:last:mb-0"
        />
      </template>
    </template>
  </UChatMessages>

  <UChatPrompt
    v-model="input"
    :error="chat.error"
    @submit="onSubmit"
  >
    <UChatPromptSubmit
      :status="chat.status"
      @stop="chat.stop()"
      @reload="chat.regenerate()"
    />
  </UChatPrompt>
</template>
```

\> \[!NOTE]
\> In this example, we use the \`MDC\` component from \[\`@nuxtjs/mdc\`]\(https\://github.com/nuxt-modules/mdc) to render messages as Markdown. As Nuxt UI provides pre-styled prose components, your content will be automatically styled.

\> \[!TIP]
\> See: /blog/how-to-build-an-ai-chat
\> Read the full Build an AI Chatbot tutorial for a step-by-step guide.


# ChatMessage

## Usage

The ChatMessage component renders an `<article>` element for a `user` or `assistant` chat message.

```vue
<template>
  <u-chat-message :avatar={"src":"https://github.com/benjamincanac.png","loading":"lazy"} :parts=[{"type":"text","id":"1","text":"Hello! Tell me more about building AI chatbots with Nuxt UI."}] id=1 role=user side=right variant=soft />
</template>
```

\> \[!TIP]
\> See: /docs/components/chat-messages
\> Use the \`ChatMessages\` component to display a list of chat messages.

### Parts

Use the `parts` prop to display the message content using the AI SDK format.

```vue
<template>
  <UChatMessage role="user" id="1" />
</template>
```

\> \[!NOTE]
\> The \`parts\` prop is the recommended format for the AI SDK. Each part has a \`type\` (e.g., 'text') and corresponding content. The ChatMessage component also supports the deprecated \`content\` prop for backward compatibility.

### Side

Use the `side` prop to display the message on the left or right.

```vue
<template>
  <UChatMessage side="right" role="user" id="1" />
</template>
```

\> \[!NOTE]
\> When using the \[\`ChatMessages\`]\(/docs/components/chat-messages) component, the \`side\` prop is set to \`left\` for \`assistant\` messages and \`right\` for \`user\` messages.

### Variant

Use the `variant` prop to change style of the message.

```vue
<template>
  <UChatMessage variant="soft" role="user" id="1" />
</template>
```

\> \[!NOTE]
\> When using the \[\`ChatMessages\`]\(/docs/components/chat-messages) component, the \`variant\` prop is set to \`naked\` for \`assistant\` messages and \`soft\` for \`user\` messages.

### Icon

Use the `icon` prop to display an [Icon](https://ui.nuxt.com/docs/components/icon) component next to the message.

```vue
<template>
  <UChatMessage icon="i-lucide-user" variant="soft" side="right" role="user" id="1" />
</template>
```

### Avatar

Use the `avatar` prop to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) component next to the message.

```vue
<template>
  <UChatMessage variant="soft" side="right" role="user" id="1" />
</template>
```

You can also use the `avatar.icon` prop to display an icon as the avatar.

```vue
<template>
  <UChatMessage role="assistant" id="1" />
</template>
```

### Actions

Use the `actions` prop to display actions below the message that will be displayed when hovering over the message.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UChatMessage role="user" id="1" />
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

## API

### Props

```ts
/**
 * Props for the ChatMessage component
 */
interface ChatMessageProps {
  /**
   * A unique identifier for the message.
   */
  id: string;
  /**
   * The role of the message.
   */
  role: "system" | "user" | "assistant";
  /**
   * The parts of the message. Use this for rendering the message in the UI.
   * 
   * System messages should be avoided (set the system prompt on the server instead).
   * They can have text parts.
   * 
   * User messages can have text parts and file parts.
   * 
   * Assistant messages can have text, reasoning, tool invocation, and file parts.
   */
  parts: UIMessagePart<UIDataTypes, UITools>[];
  /**
   * The element or component this component should render as.
   * @default "\"article\""
   */
  as?: any;
  icon?: any;
  avatar?: (AvatarProps & { [key: string]: any; }) | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | "naked" | undefined;
  side?: "left" | "right" | undefined;
  /**
   * Display a list of actions under the message.
   * The `label` will be used in a tooltip.
   * `{ size: 'xs', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  actions?: (Omit<ButtonProps, "onClick"> & { onClick?: ((e: MouseEvent, message: UIMessage<unknown, UIDataTypes, UITools>) => void) | undefined; })[] | undefined;
  /**
   * Render the message in a compact style.
   * This is done automatically when used inside a `UChatPalette`{lang="ts-type"}.
   */
  compact?: boolean | undefined;
  content?: string | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; content?: ClassNameValue; actions?: ClassNameValue; } | undefined;
  /**
   * The metadata of the message.
   */
  metadata?: unknown;
}
```

### Slots

```ts
/**
 * Slots for the ChatMessage component
 */
interface ChatMessageSlots {
  leading(): any;
  content(): any;
  actions(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatMessage: {
      slots: {
        root: 'group/message relative w-full',
        container: 'relative flex items-start',
        leading: 'inline-flex items-center justify-center min-h-6',
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        content: 'relative text-pretty min-w-0 *:first:mt-0 *:last:mb-0',
        actions: [
          'opacity-0 group-hover/message:opacity-100 absolute bottom-0 flex items-center',
          'transition-opacity'
        ]
      },
      variants: {
        variant: {
          solid: {
            content: 'bg-inverted text-inverted'
          },
          outline: {
            content: 'bg-default ring ring-default'
          },
          soft: {
            content: 'bg-elevated/50'
          },
          subtle: {
            content: 'bg-elevated/50 ring ring-default'
          },
          naked: {
            content: ''
          }
        },
        side: {
          left: {
            container: 'rtl:justify-end'
          },
          right: {
            container: 'ltr:justify-end ms-auto max-w-[75%]'
          }
        },
        leading: {
          true: ''
        },
        actions: {
          true: ''
        },
        compact: {
          true: {
            root: 'scroll-mt-3',
            container: 'gap-1.5 pb-3',
            content: 'space-y-2',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs'
          },
          false: {
            root: 'scroll-mt-4 sm:scroll-mt-6',
            container: 'gap-3 pb-8',
            content: 'space-y-4',
            leadingIcon: 'size-8',
            leadingAvatarSize: 'md'
          }
        }
      },
      compoundVariants: [
        {
          compact: true,
          actions: true,
          class: {
            container: 'pb-8'
          }
        },
        {
          leading: true,
          compact: false,
          side: 'left',
          class: {
            actions: 'left-11'
          }
        },
        {
          leading: true,
          compact: true,
          side: 'left',
          class: {
            actions: 'left-6.5'
          }
        },
        {
          variant: [
            'solid',
            'outline',
            'soft',
            'subtle'
          ],
          compact: false,
          class: {
            content: 'px-4 py-3 rounded-lg min-h-12',
            leading: 'mt-2'
          }
        },
        {
          variant: [
            'solid',
            'outline',
            'soft',
            'subtle'
          ],
          compact: true,
          class: {
            content: 'px-2 py-1 rounded-lg min-h-8',
            leading: 'mt-1'
          }
        },
        {
          variant: 'naked',
          side: 'left',
          class: {
            content: 'w-full'
          }
        }
      ],
      defaultVariants: {
        variant: 'naked'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatMessage.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-message.ts).


# ChatMessages

## Usage

The ChatMessages component displays a list of [ChatMessage](https://ui.nuxt.com/docs/components/chat-message) components using either the default slot or the `messages` prop.

```vue {2,8}
<template>
  <UChatMessages>
    <UChatMessage
      v-for="(message, index) in messages"
      :key="index"
      v-bind="message"
    />
  </UChatMessages>
</template>
```

\> \[!NOTE]
\> This component is purpose-built for AI chatbots with features like\:Initial scroll to the bottom upon loading (\[\`shouldScrollToBottom\`]\(#should-scroll-to-bottom)).Continuous scrolling down as new messages arrive (\[\`shouldAutoScroll\`]\(#should-auto-scroll)).An "Auto scroll" button appears when scrolled up, allowing users to jump back to the latest messages (\[\`autoScroll\`]\(#auto-scroll)).A loading indicator displays while the assistant is processing (\[\`status\`]\(#status)).Submitted messages are scrolled to the top of the viewport and the height of the last user message is dynamically adjusted.

### Messages

Use the `messages` prop to display a list of chat messages.

```vue
<template>
  <UChatMessages />
</template>
```

### Status

Use the `status` prop to display a visual indicator when the assistant is processing.

```vue
<template>
  <UChatMessages status="submitted" />
</template>
```

\> \[!NOTE]
\> Here's the detail of the different statuses from the AI SDK Chat class:\`submitted\`: The message has been sent to the API and we're awaiting the start of the response stream.\`streaming\`: The response is actively streaming in from the API, receiving chunks of data.\`ready\`: The full response has been received and processed; a new user message can be submitted.\`error\`: An error occurred during the API request, preventing successful completion.

### User

Use the `user` prop to change the [ChatMessage](https://ui.nuxt.com/docs/components/chat-message) props for `user` messages. Defaults to:

- `side: 'right'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `variant: 'soft'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<template>
  <UChatMessages />
</template>
```

### Assistant

Use the `assistant` prop to change the [ChatMessage](https://ui.nuxt.com/docs/components/chat-message) props for `assistant` messages. Defaults to:

- `side: 'left'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `variant: 'naked'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<template>
  <UChatMessages />
</template>
```

### Auto Scroll

Use the `auto-scroll` prop to customize or hide the auto scroll button (with `false` value) displayed when scrolling to the top of the chat. Defaults to:

- `color: 'neutral'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `variant: 'outline'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <UChatMessages :should-scroll-to-bottom="false" />
</template>
```

### Auto Scroll Icon

Use the `auto-scroll-icon` prop to customize the auto scroll button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-arrow-down`.

```vue
<template>
  <UChatMessages auto-scroll-icon="i-lucide-chevron-down" :should-scroll-to-bottom="false" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.arrowDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.arrowDown\` key.

### Should Auto Scroll

Use the `should-auto-scroll` prop to enable/disable continuous auto scroll while messages are streaming. Defaults to `false`.

```vue
<template>
  <UChatMessages :messages="messages" should-auto-scroll />
</template>
```

### Should Scroll To Bottom

Use the `should-scroll-to-bottom` prop to enable/disable bottom auto scroll when the component is mounted. Defaults to `true`.

```vue
<template>
  <UChatMessages :messages="messages" :should-scroll-to-bottom="false" />
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

### With indicator slot

You can customize the loading indicator that appears when the status is `submitted`.

```vue [ChatMessagesIndicatorSlotExample.vue]
<template>
  <UChatMessages
    :messages="[
      {
        id: '1',
        role: 'user',
        parts: [{ type: 'text', text: 'Hello! Can you help me with something?' }]
      }
    ]"
    status="submitted"
    :should-scroll-to-bottom="false"
    :user="{
      avatar: { icon: 'i-lucide-user' },
      variant: 'soft',
      side: 'right'
    }"
  >
    <template #indicator>
      <UButton
        class="px-0"
        color="neutral"
        variant="link"
        loading
        loading-icon="i-lucide-loader"
        label="Thinking..."
      />
    </template>
  </UChatMessages>
</template>
```

## API

### Props

```ts
/**
 * Props for the ChatMessages component
 */
interface ChatMessagesProps {
  messages?: UIMessage<unknown, UIDataTypes, UITools>[] | undefined;
  status?: ChatStatus | undefined;
  /**
   * Whether to automatically scroll to the bottom when a message is streaming.
   * @default "false"
   */
  shouldAutoScroll?: boolean | undefined;
  /**
   * Whether to scroll to the bottom on mounted.
   * @default "true"
   */
  shouldScrollToBottom?: boolean | undefined;
  /**
   * Display an auto scroll button.
   * `{ size: 'md', color: 'neutral', variant: 'outline' }`{lang="ts-type"}
   * @default "true"
   */
  autoScroll?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the auto scroll button.
   */
  autoScrollIcon?: any;
  /**
   * The `user` messages props.
   * `{ side: 'right', variant: 'soft' }`{lang="ts-type"}
   */
  user?: Pick<ChatMessageProps, "ui" | "variant" | "icon" | "avatar" | "side" | "actions"> | undefined;
  /**
   * The `assistant` messages props.
   * `{ side: 'left', variant: 'naked' }`{lang="ts-type"}
   */
  assistant?: Pick<ChatMessageProps, "ui" | "variant" | "icon" | "avatar" | "side" | "actions"> | undefined;
  /**
   * Render the messages in a compact style.
   * This is done automatically when used inside a `UChatPalette`{lang="ts-type"}.
   */
  compact?: boolean | undefined;
  /**
   * The spacing offset for the last message in px. Can be useful when the prompt is sticky for example.
   * @default "0"
   */
  spacingOffset?: number | undefined;
  ui?: { root?: ClassNameValue; indicator?: ClassNameValue; viewport?: ClassNameValue; autoScroll?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChatMessages component
 */
interface ChatMessagesSlots {
  leading(): any;
  content(): any;
  actions(): any;
  default(): any;
  indicator(): any;
  viewport(): any;
}
```

\> \[!TIP]
\> You can use all the slots of the \[\`ChatMessage\`]\(/docs/components/chat-message#slots) component inside ChatMessages, they are automatically forwarded allowing you to customize individual messages when using the \`messages\` prop.
\> \`\`\`vue
\> \<template>
\> \<UChatMessages \:messages="messages" \:status="status">
\> \<template #content="{ message }">
\> \<template
\> v-for="(part, index) in message.parts"
\> \:key="\`${message.id}-${part.type}-${index}\`"
\> >
\> \<MDC
\> \:value="part.text"
\> \:cache-key="\`${message.id}-${index}\`"
\> class="\*:first\:mt-0 \*:last\:mb-0"
\> />
\> \</template>
\> \</template>
\> \</UChatMessages>
\> \</template>
\>
\> \`\`\`

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatMessages: {
      slots: {
        root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>article]:last-of-type:min-h-(--last-message-height)',
        indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-elevated [&>*:nth-child(1)]:animate-[bounce_1s_infinite] [&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] [&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
        viewport: 'absolute inset-x-0 top-[86%] data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
        autoScroll: 'rounded-full absolute right-1/2 translate-x-1/2 bottom-0'
      },
      variants: {
        compact: {
          true: '',
          false: ''
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatMessages.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-messages.ts).


# ChatPalette

## Usage

The ChatPalette component is a structured layout wrapper that organizes [ChatMessages](https://ui.nuxt.com/docs/components/chat-messages) in a scrollable content area and [ChatPrompt](https://ui.nuxt.com/docs/components/chat-prompt) in a fixed bottom section, creating cohesive chatbot interfaces for modals, slideovers, or drawers.

```vue {2,8}
<template>
  <UChatPalette>
    <UChatMessages />

    <template #prompt>
      <UChatPrompt />
    </template>
  </UChatPalette>
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

### Within a Modal

You can use the ChatPalette component inside a [Modal](https://ui.nuxt.com/docs/components/modal)'s content.

```vue [ChatPaletteModalExample.vue]
<script setup lang="ts">
import { isTextUIPart } from 'ai'
import type { UIMessage } from 'ai'
import { Chat } from '@ai-sdk/vue'

const messages: UIMessage[] = [{
  id: '1',
  role: 'user',
  parts: [{ type: 'text', text: 'What is Nuxt UI?' }]
}, {
  id: '2',
  role: 'assistant',
  parts: [{ type: 'text', text: 'Nuxt UI is a Vue component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides 125+ accessible components for building modern web apps.' }]
}]
const input = ref('')

const chat = new Chat({
  messages
})

function onSubmit() {
  if (!input.value.trim()) return

  chat.sendMessage({ text: input.value })

  input.value = ''
}

const ui = {
  prose: {
    p: { base: 'my-2 leading-6' },
    li: { base: 'my-0.5 leading-6' },
    ul: { base: 'my-2' },
    ol: { base: 'my-2' },
    h1: { base: 'text-xl my-2' },
    h2: { base: 'text-lg my-2' },
    h3: { base: 'text-base my-2' },
    h4: { base: 'text-sm my-2' },
    pre: { root: 'my-2' },
    table: { root: 'my-2' },
    hr: { base: 'my-2' }
  }
}
</script>

<template>
  <UModal open :ui="{ content: 'sm:max-w-3xl sm:h-[28rem]' }">
    <template #content>
      <UTheme :ui="ui">
        <UChatPalette>
          <UChatMessages
            :messages="chat.messages"
            :status="chat.status"
            :user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
            :assistant="{ icon: 'i-lucide-bot' }"
          >
            <template #content="{ message }">
              <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
                <MDC
                  v-if="isTextUIPart(part)"
                  :value="part.text"
                  :cache-key="`${message.id}-${index}`"
                  class="*:first:mt-0 *:last:mb-0"
                />
              </template>
            </template>
          </UChatMessages>

          <template #prompt>
            <UChatPrompt
              v-model="input"
              icon="i-lucide-search"
              variant="naked"
              :error="chat.error"
              @submit="onSubmit"
            />
          </template>
        </UChatPalette>
      </UTheme>
    </template>
  </UModal>
</template>
```

### Within ContentSearch

You can use the ChatPalette component conditionally inside [ContentSearch](https://ui.nuxt.com/docs/components/content-search)'s content to display a chatbot interface when a user selects an item.

```vue [ChatPaletteContentSearchExample.vue]
<script setup lang="ts">
import { isTextUIPart } from 'ai'
import type { UIMessage } from 'ai'
import { Chat } from '@ai-sdk/vue'

const messages: UIMessage[] = []
const input = ref('')

const groups = computed(() => [{
  id: 'ai',
  ignoreFilter: true,
  items: [{
    label: searchTerm.value ? `Ask AI for “${searchTerm.value}”` : 'Ask AI',
    icon: 'i-lucide-bot',
    onSelect: (e: any) => {
      e.preventDefault()

      ai.value = true

      if (searchTerm.value) {
        messages.push({
          id: '1',
          role: 'user',
          parts: [{ type: 'text', text: searchTerm.value }]
        })

        chat.regenerate()
      }
    }
  }]
}])

const ai = ref(false)
const searchTerm = ref('')

const chat = new Chat({
  messages
})

function onSubmit() {
  if (!input.value.trim()) return

  chat.sendMessage({ text: input.value })

  input.value = ''
}

function onClose(e: Event) {
  e.preventDefault()

  ai.value = false
}

const ui = {
  prose: {
    p: { base: 'my-2 leading-6' },
    li: { base: 'my-0.5 leading-6' },
    ul: { base: 'my-2' },
    ol: { base: 'my-2' },
    h1: { base: 'text-xl my-2' },
    h2: { base: 'text-lg my-2' },
    h3: { base: 'text-base my-2' },
    h4: { base: 'text-sm my-2' },
    pre: { root: 'my-2' },
    table: { root: 'my-2' },
    hr: { base: 'my-2' }
  }
}
</script>

<template>
  <UContentSearch v-model:search-term="searchTerm" open :groups="groups">
    <template v-if="ai" #content>
      <UTheme :ui="ui">
        <UChatPalette>
          <UChatMessages
            :messages="chat.messages"
            :status="chat.status"
            :user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
            :assistant="{ icon: 'i-lucide-bot' }"
          >
            <template #content="{ message }">
              <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
                <MDC
                  v-if="isTextUIPart(part)"
                  :value="part.text"
                  :cache-key="`${message.id}-${index}`"
                  class="*:first:mt-0 *:last:mb-0"
                />
              </template>
            </template>
          </UChatMessages>

          <template #prompt>
            <UChatPrompt
              v-model="input"
              icon="i-lucide-search"
              variant="naked"
              :error="chat.error"
              @submit="onSubmit"
              @close="onClose"
            />
          </template>
        </UChatPalette>
      </UTheme>
    </template>
  </UContentSearch>
</template>
```

## API

### Props

```ts
/**
 * Props for the ChatPalette component
 */
interface ChatPaletteProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { root?: ClassNameValue; prompt?: ClassNameValue; close?: ClassNameValue; content?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChatPalette component
 */
interface ChatPaletteSlots {
  default(): any;
  prompt(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatPalette: {
      slots: {
        root: 'relative flex-1 flex flex-col min-h-0 min-w-0',
        prompt: 'px-0 rounded-t-none border-t border-default',
        close: '',
        content: 'overflow-y-auto flex-1 flex flex-col py-3'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatPalette.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-palette.ts).


# ChatPrompt

## Usage

The ChatPrompt component renders a `<form>` element and extends the [Textarea](https://ui.nuxt.com/docs/components/textarea) component so you can pass any property such as `icon`, `placeholder`, `autofocus`, etc.

```vue
<template>
  <u-chat-prompt variant=subtle>
  <u-chat-prompt-submit color=neutral />
  <template v-slot:footer=>
  <u-select :items=[{"label":"Claude Opus 4.6","value":"claude-opus-4.6","icon":"i-simple-icons-anthropic"},{"label":"Gemini 3 Pro","value":"gemini-3-pro","icon":"i-simple-icons-googlegemini"},{"label":"GPT-5","value":"gpt-5","icon":"i-simple-icons-openai"}] icon=i-simple-icons-anthropic modelValue=claude-opus-4.6 placeholder=Select a model variant=ghost /></template></u-chat-prompt>
</template>
```

\> \[!NOTE]
\> The ChatPrompt handles the following events\:The form is submitted when the user presses or when the user clicks on the submit button.The textarea is blurred when is pressed and emits a \`close\` event.

### Variant

Use the `variant` prop to change the style of the prompt. Defaults to `outline`.

```vue
<template>
  <UChatPrompt variant="soft" />
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

### As a starting point

You can also use it as a starting point for a chat interface.

```vue [pages/index.vue] {2,4,8-15,24,26}
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'

const input = ref('')

const chat = new Chat()

async function onSubmit() {
  chat.sendMessage({ text: input.value })

  // Navigate to chat page after first message
  if (chat.messages.length === 1) {
    await navigateTo('/chat')
  }
}
</script>

<template>
  <UDashboardPanel>
    <template #body>
      <UContainer>
        <h1>How can I help you today?</h1>

        <UChatPrompt v-model="input" @submit="onSubmit">
          <UChatPromptSubmit :status="chat.status" />
        </UChatPrompt>
      </UContainer>
    </template>
  </UDashboardPanel>
</template>
```

## API

### Props

```ts
/**
 * Props for the ChatPrompt component
 */
interface ChatPromptProps {
  /**
   * The element or component this component should render as.
   * @default "\"form\""
   */
  as?: any;
  /**
   * The placeholder text for the textarea.
   */
  placeholder?: string | undefined;
  variant?: "outline" | "soft" | "subtle" | "naked" | undefined;
  error?: Error | undefined;
  ui?: ({ root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; base?: ClassNameValue; } & { root?: ClassNameValue; base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; }) | undefined;
  /**
   * @default "true"
   */
  autofocus?: boolean | undefined;
  disabled?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * @default "1"
   */
  rows?: number | undefined;
  autofocusDelay?: number | undefined;
  /**
   * @default "true"
   */
  autoresize?: boolean | undefined;
  autoresizeDelay?: number | undefined;
  maxrows?: number | undefined;
  /**
   * @default "\"\""
   */
  modelValue?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attributes
\> This component also supports all native \`\<textarea>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the ChatPrompt component
 */
interface ChatPromptSlots {
  header(): any;
  footer(): any;
  leading(): any;
  default(): any;
  trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ChatPrompt component
 */
interface ChatPromptEmits {
  close: (payload: [event: Event]) => void;
  submit: (payload: [event: Event]) => void;
  update:modelValue: (payload: [value: string]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `textareaRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLTextAreaElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatPrompt: {
      slots: {
        root: 'relative flex flex-col items-stretch gap-2 px-2.5 py-2 w-full rounded-lg backdrop-blur',
        header: 'flex items-center gap-1.5',
        body: 'items-start',
        footer: 'flex items-center justify-between gap-1.5',
        base: ''
      },
      variants: {
        variant: {
          outline: {
            root: 'bg-default/75 ring ring-default'
          },
          soft: {
            root: 'bg-elevated/50'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default'
          },
          naked: {
            root: ''
          }
        }
      },
      defaultVariants: {
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatPrompt.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-prompt.ts).


# ChatPromptSubmit

## Usage

The ChatPromptSubmit component is used inside the [ChatPrompt](https://ui.nuxt.com/docs/components/chat-prompt) component to submit the prompt. It automatically handles the different `status` values to control the chat.

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <u-chat-prompt-submit />
  <template v-slot:code=>
  <pre className=language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight code=<template>
    <UChatPrompt>
      <UChatPromptSubmit />
    </UChatPrompt>
  </template>
   language=vue meta= style=>
  <code __ignoreMap=>
  <span class=line>
  <span class=sMK4o>
  <</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
    <</span>
  <span class=swJcz>
  UChatPrompt</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
      <</span>
  <span class=swJcz>
  UChatPromptSubmit</span>
  <span class=sMK4o>
   />
  </span></span>
  <span class=line>
  <span class=sMK4o>
    </</span>
  <span class=swJcz>
  UChatPrompt</span>
  <span class=sMK4o>
  >
  </span></span>
  <span class=line>
  <span class=sMK4o>
  </</span>
  <span class=swJcz>
  template</span>
  <span class=sMK4o>
  >
  </span></span></code></pre></template>
</template>
```

\> \[!NOTE]
\> You can also use it inside the \`footer\` slot of the \[\`ChatPrompt\`]\(/docs/components/chat-prompt) component.

### Ready

When its status is `ready`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, use the `color`, `variant` and `icon` props to customize the Button. Defaults to:

- `color="primary"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `variant="solid"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon="i-lucide-arrow-up"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<template>
  <UChatPromptSubmit color="primary" variant="solid" icon="i-lucide-arrow-up" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.arrowUp\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.arrowUp\` key.

### Submitted

When its status is `submitted`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, use the `submitted-color`, `submitted-variant` and `submitted-icon` props to customize the Button. Defaults to:

- `submittedColor="neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `submittedVariant="subtle"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `submittedIcon="i-lucide-square"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

\> \[!NOTE]
\> The \`stop\` event is emitted when the user clicks on the Button.

```vue
<template>
  <UChatPromptSubmit submitted-color="neutral" submitted-variant="subtle" submitted-icon="i-lucide-square" status="submitted" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.stop\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.stop\` key.

### Streaming

When its status is `streaming`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, use the `streaming-color`, `streaming-variant` and `streaming-icon` props to customize the Button. Defaults to:

- `streamingColor="neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `streamingVariant="subtle"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `streamingIcon="i-lucide-square"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

\> \[!NOTE]
\> The \`stop\` event is emitted when the user clicks on the Button.

```vue
<template>
  <UChatPromptSubmit streaming-color="neutral" streaming-variant="subtle" streaming-icon="i-lucide-square" status="streaming" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.stop\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.stop\` key.

### Error

When its status is `error`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, use the `error-color`, `error-variant` and `error-icon` props to customize the Button. Defaults to:

- `errorColor="error"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `errorVariant="soft"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `errorIcon="i-lucide-rotate-ccw"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

\> \[!NOTE]
\> The \`reload\` event is emitted when the user clicks on the Button.

```vue
<template>
  <UChatPromptSubmit error-color="error" error-variant="soft" error-icon="i-lucide-rotate-ccw" status="error" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.reload\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.reload\` key.

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

## API

### Props

```ts
/**
 * Props for the ChatPromptSubmit component
 */
interface ChatPromptSubmitProps {
  /**
   * @default "\"ready\""
   */
  status?: ChatStatus | undefined;
  /**
   * The icon displayed in the button when the status is `ready`.
   */
  icon?: any;
  /**
   * The color of the button when the status is `ready`.
   */
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The variant of the button when the status is `ready`.
   */
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The icon displayed in the button when the status is `streaming`.
   */
  streamingIcon?: any;
  /**
   * The color of the button when the status is `streaming`.
   * @default "\"neutral\""
   */
  streamingColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The variant of the button when the status is `streaming`.
   * @default "\"subtle\""
   */
  streamingVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The icon displayed in the button when the status is `submitted`.
   */
  submittedIcon?: any;
  /**
   * The color of the button when the status is `submitted`.
   * @default "\"neutral\""
   */
  submittedColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The variant of the button when the status is `submitted`.
   * @default "\"subtle\""
   */
  submittedVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The icon displayed in the button when the status is `error`.
   */
  errorIcon?: any;
  /**
   * The color of the button when the status is `error`.
   * @default "\"error\""
   */
  errorColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The variant of the button when the status is `error`.
   * @default "\"soft\""
   */
  errorVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  ui?: ({ base?: ClassNameValue; } & { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; }) | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  label?: string | undefined;
  activeColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  activeVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the ChatPromptSubmit component
 */
interface ChatPromptSubmitSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ChatPromptSubmit component
 */
interface ChatPromptSubmitEmits {
  stop: (payload: [event: MouseEvent]) => void;
  reload: (payload: [event: MouseEvent]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatPromptSubmit: {
      slots: {
        base: ''
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatPromptSubmit.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-prompt-submit.ts).


# ChatReasoning

## Usage

The ChatReasoning component renders a collapsible block that displays AI reasoning or thinking content. It auto-opens during streaming and auto-closes after.

```vue [ChatReasoningExample.vue]
<script setup lang="ts">
const streaming = ref(false)
const text = ref('')

async function simulateStreaming() {
  streaming.value = true
  text.value = ''

  const content = 'The user is asking about Vue components. I should explain the Composition API pattern and how it relates to their question about reactive state management. Let me think about the best way to structure this response.\n\nFirst, I need to consider the key differences between the Options API and Composition API. The Composition API was introduced in Vue 3 to address limitations of the Options API when building large-scale applications.\n\nFor reactive state management specifically, the Composition API offers ref() for primitive values and reactive() for objects.'

  for (const char of content) {
    text.value += char
    await new Promise(resolve => setTimeout(resolve, 10))
  }

  streaming.value = false
}

onMounted(simulateStreaming)
</script>

<template>
  <UChatReasoning
    :text="text"
    :streaming="streaming"
    class="w-60"
  />
</template>
```

\> \[!NOTE]
\> See: /docs/composables/use-scroll-shadow
\> The body content uses the \`useScrollShadow\` composable to apply fade shadows when overflowing.

### Text

Use the `text` prop to set the reasoning content. The text is displayed inside the collapsible body.

```vue
<template>
  <UChatReasoning text="The user is asking about Vue components..." />
</template>
```

### Streaming

Use the `streaming` prop to indicate active reasoning. The component auto-opens when streaming starts and auto-closes when it ends.

```vue
<template>
  <UChatReasoning streaming text="The user is asking about Vue components..." />
</template>
```

\> \[!TIP]
\> Use the \`isReasoningStreaming\` utility from \`@nuxt/ui/utils/ai\` to determine if a reasoning part is currently being streamed.

### Shimmer

When streaming, the trigger label uses the [`ChatShimmer`](https://ui.nuxt.com/docs/components/chat-shimmer) component. Use the `shimmer` prop to customize its `duration` and `spread`.

```vue
<template>
  <UChatReasoning streaming text="The user is asking about Vue components..." />
</template>
```

### Icon

Use the `icon` prop to display an [Icon](https://ui.nuxt.com/docs/components/icon) component next to the trigger.

```vue
<template>
  <UChatReasoning icon="i-lucide-brain" text="The user is asking about Vue components..." />
</template>
```

### Chevron

Use the `chevron` prop to change the position of the chevron icon.

\> \[!NOTE]
\> When \`chevron\` is set to \`leading\` with an \`icon\`, the icon swaps with the chevron on hover and when open.

```vue
<template>
  <UChatReasoning chevron="leading" icon="i-lucide-brain" text="The user is asking about Vue components..." />
</template>
```

### Chevron Icon

Use the `chevron-icon` prop to customize the chevron [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-chevron-down`.

```vue
<template>
  <UChatReasoning chevron-icon="i-lucide-arrow-down" text="The user is asking about Vue components..." />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

## API

### Props

```ts
/**
 * Props for the ChatReasoning component
 */
interface ChatReasoningProps {
  /**
   * The reasoning text content to display.
   */
  text?: string | undefined;
  /**
   * Whether the reasoning content is currently streaming.
   * @default "false"
   */
  streaming?: boolean | undefined;
  /**
   * The duration in seconds that the AI spent reasoning.
   * If not provided, it will be calculated automatically based on streaming time.
   */
  duration?: number | undefined;
  /**
   * The icon displayed next to the trigger.
   */
  icon?: any;
  /**
   * The position of the chevron icon.
   * @default "\"trailing\""
   */
  chevron?: "leading" | "trailing" | undefined;
  /**
   * The icon displayed as the chevron.
   */
  chevronIcon?: any;
  /**
   * The delay in milliseconds before auto-closing when streaming ends.
   * Set to `0` to disable auto-close.
   * @default "500"
   */
  autoCloseDelay?: number | undefined;
  /**
   * Customize the [`ChatShimmer`](https://ui.nuxt.com/docs/components/chat-shimmer) component when streaming.
   */
  shimmer?: Partial<Omit<ChatShimmerProps, "text">> | undefined;
  ui?: { root?: ClassNameValue; trigger?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; chevronIcon?: ClassNameValue; label?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the collapsible.
   */
  disabled?: boolean | undefined;
  /**
   * The controlled open state of the collapsible. Can be binded with `v-model`.
   * @default "undefined"
   */
  open?: boolean | undefined;
  /**
   * The open state of the collapsible when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "false"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChatReasoning component
 */
interface ChatReasoningSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ChatReasoning component
 */
interface ChatReasoningEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatReasoning: {
      slots: {
        root: '',
        trigger: [
          'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
          'transition-colors'
        ],
        leading: 'relative size-4 shrink-0',
        leadingIcon: 'size-4 shrink-0',
        chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        label: 'truncate',
        trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
        body: 'max-h-[200px] pt-2 overflow-y-auto text-sm text-dimmed whitespace-pre-wrap'
      },
      variants: {
        chevron: {
          leading: {
            leadingIcon: 'group-hover:opacity-0'
          },
          trailing: ''
        },
        alone: {
          false: {
            leadingIcon: [
              'absolute inset-0 group-data-[state=open]:opacity-0',
              'transition-opacity duration-200'
            ],
            chevronIcon: [
              'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
              'transition-[rotate,opacity] duration-200'
            ]
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatReasoning.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-reasoning.ts).


# ChatShimmer

## Usage

The ChatShimmer component renders an element with an animated shimmer gradient over text, commonly used to indicate streaming or loading states in chat interfaces.

\> \[!NOTE]
\> This component is automatically used by the \[\`ChatTool\`]\(/docs/components/chat-tool) and \[\`ChatReasoning\`]\(/docs/components/chat-reasoning) components when streaming.

### Text

Use the `text` prop to set the shimmer text.

```vue
<template>
  <UChatShimmer text="Thinking..." />
</template>
```

### Duration

Use the `duration` prop to control the animation speed in seconds.

```vue
<template>
  <UChatShimmer text="Thinking..." :duration="4" />
</template>
```

### Spread

Use the `spread` prop to control the width of the shimmer highlight. The actual spread is computed as `text.length * spread` in pixels.

```vue
<template>
  <UChatShimmer text="Thinking..." :spread="5" />
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

## API

### Props

```ts
/**
 * Props for the ChatShimmer component
 */
interface ChatShimmerProps {
  /**
   * The text to display with the shimmer effect.
   */
  text: string;
  /**
   * The element or component this component should render as.
   * @default "\"span\""
   */
  as?: any;
  /**
   * The duration of the shimmer animation in seconds.
   * @default "2"
   */
  duration?: number | undefined;
  /**
   * The spread multiplier for the shimmer highlight. The actual spread is computed as `text.length * spread` in pixels.
   * @default "2"
   */
  spread?: number | undefined;
  ui?: {} | undefined;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatShimmer: {
      base: 'text-transparent bg-clip-text bg-no-repeat bg-size-[calc(200%+var(--spread)*2+2px)_100%,auto] bg-[image:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--ui-text-highlighted),#0000_calc(50%+var(--spread))),linear-gradient(var(--ui-text-muted),var(--ui-text-muted))] animate-[shimmer_var(--duration)_linear_infinite] rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite] will-change-[background-position]'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatShimmer.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-shimmer.ts).


# ChatTool

## Usage

The ChatTool component renders a collapsible block that displays AI tool invocation status, such as "Searching components" or "Reading documentation". When a default slot is provided, it becomes collapsible to reveal tool output.

```vue [ChatToolExample.vue]
<script setup lang="ts">
const streaming = ref(true)
const result = ref(`$ pnpm run lint

> eslint .

✔ No lint errors found.
`)

let timer: ReturnType<typeof setTimeout> | undefined

onMounted(() => {
  timer = setTimeout(() => {
    streaming.value = false
  }, 5000)
})

onUnmounted(() => {
  clearTimeout(timer)
})
</script>

<template>
  <UChatTool
    :text="streaming ? 'Running lint checks' : 'Lint checks completed'"
    suffix="cd, pnpm run"
    :streaming="streaming"
    icon="i-lucide-terminal"
    variant="card"
    chevron="leading"
    class="w-80"
  >
    <pre language="bash" v-text="result" />
  </UChatTool>
</template>
```

### Text

Use the `text` prop to set the tool status text.

```vue
<template>
  <UChatTool text="Searched components" />
</template>
```

### Suffix

Use the `suffix` prop to display secondary text after the main label.

```vue
<template>
  <UChatTool text="Reading component" suffix="Button" />
</template>
```

### Streaming

Use the `streaming` prop to indicate the tool is actively running. The text displays a shimmer animation.

```vue
<template>
  <UChatTool streaming text="Searching components..." />
</template>
```

\> \[!TIP]
\> Use the \`isToolStreaming\` utility from \`@nuxt/ui/utils/ai\` to determine if a tool part is still running.

### Shimmer

When streaming, the trigger label uses the [`ChatShimmer`](https://ui.nuxt.com/docs/components/chat-shimmer) component. Use the `shimmer` prop to customize its `duration` and `spread`.

```vue
<template>
  <UChatTool streaming text="Searching components..." />
</template>
```

### Icon

Use the `icon` prop to display an [Icon](https://ui.nuxt.com/docs/components/icon) component next to the trigger.

```vue
<template>
  <UChatTool icon="i-lucide-search" text="Searched components" />
</template>
```

### Loading

Use the `loading` prop to show a loading indicator. Use the `loading-icon` prop to customize the loading icon.

```vue
<template>
  <UChatTool loading text="Searching components..." />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Chevron

Use the `chevron` prop to change the position of the chevron icon.

\> \[!NOTE]
\> When \`chevron\` is set to \`leading\` with an \`icon\`, the icon swaps with the chevron on hover and when open.

```vue
<template>
  <UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
    Tool output content
  </UChatTool>
</template>
```

### Chevron Icon

Use the `chevron-icon` prop to customize the chevron [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-chevron-down`.

```vue
<template>
  <UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
    Tool output content
  </UChatTool>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Variant

Use the `variant` prop to change the visual style. Defaults to `inline`.

```vue
<template>
  <UChatTool variant="card" text="Searched components" icon="i-lucide-search" chevron="trailing">
    Tool output content
  </UChatTool>
</template>
```

## Examples

\> \[!TIP]
\> See: /docs/components/chat
\> Check the Chat overview page for installation instructions, server setup and usage examples.

## API

### Props

```ts
/**
 * Props for the ChatTool component
 */
interface ChatToolProps {
  /**
   * The text content to display.
   */
  text?: string | undefined;
  /**
   * The suffix text displayed after the main text.
   */
  suffix?: string | undefined;
  /**
   * The icon displayed next to the trigger.
   */
  icon?: any;
  /**
   * Whether the tool is in a loading state.
   * @default "false"
   */
  loading?: boolean | undefined;
  /**
   * The icon displayed when loading.
   */
  loadingIcon?: any;
  /**
   * Whether the tool content is currently streaming.
   * @default "false"
   */
  streaming?: boolean | undefined;
  /**
   * The visual variant of the tool display.
   * @default "\"inline\""
   */
  variant?: "card" | "inline" | undefined;
  /**
   * The position of the chevron icon.
   * @default "\"trailing\""
   */
  chevron?: "leading" | "trailing" | undefined;
  /**
   * The icon displayed as the chevron.
   */
  chevronIcon?: any;
  /**
   * Customize the [`ChatShimmer`](https://ui.nuxt.com/docs/components/chat-shimmer) component when streaming.
   */
  shimmer?: Partial<Omit<ChatShimmerProps, "text">> | undefined;
  ui?: { root?: ClassNameValue; trigger?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; chevronIcon?: ClassNameValue; label?: ClassNameValue; suffix?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the collapsible.
   */
  disabled?: boolean | undefined;
  /**
   * The controlled open state of the collapsible. Can be binded with `v-model`.
   * @default "undefined"
   */
  open?: boolean | undefined;
  /**
   * The open state of the collapsible when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "false"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ChatTool component
 */
interface ChatToolSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ChatTool component
 */
interface ChatToolEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chatTool: {
      slots: {
        root: '',
        trigger: [
          'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
          'transition-colors'
        ],
        leading: 'relative size-4 shrink-0',
        leadingIcon: 'size-4 shrink-0',
        chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        label: 'truncate',
        suffix: 'text-dimmed ms-1',
        trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
        body: 'text-sm text-dimmed whitespace-pre-wrap'
      },
      variants: {
        variant: {
          inline: {
            body: 'pt-2'
          },
          card: {
            root: 'rounded-md ring ring-default overflow-hidden',
            trigger: 'px-2 py-1',
            trailingIcon: 'ms-auto',
            body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
          }
        },
        chevron: {
          leading: {
            leadingIcon: 'group-hover:opacity-0'
          },
          trailing: ''
        },
        loading: {
          true: {
            leadingIcon: 'animate-spin'
          }
        },
        alone: {
          false: {
            leadingIcon: [
              'absolute inset-0 group-data-[state=open]:opacity-0',
              'transition-opacity duration-200'
            ],
            chevronIcon: [
              'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
              'transition-[rotate,opacity] duration-200'
            ]
          }
        }
      },
      defaultVariants: {
        variant: 'inline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ChatTool.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chat-tool.ts).


# Checkbox

## Usage

Use the `v-model` directive to control the checked state of the Checkbox.

```vue
<template>
  <UCheckbox model-value />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UCheckbox default-value />
</template>
```

### Indeterminate

Use the `indeterminate` value in the `v-model` directive or `default-value` prop to set the Checkbox to an [indeterminate state](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes){rel="&#x22;nofollow&#x22;"}.

```vue
<template>
  <UCheckbox default-value="indeterminate" />
</template>
```

### Indeterminate Icon

Use the `indeterminate-icon` prop to customize the indeterminate icon. Defaults to `i-lucide-minus`.

```vue
<template>
  <UCheckbox default-value="indeterminate" indeterminate-icon="i-lucide-plus" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.minus\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.minus\` key.

### Label

Use the `label` prop to set the label of the Checkbox.

```vue
<template>
  <UCheckbox label="Check me" />
</template>
```

When using the `required` prop, an asterisk is added next to the label.

```vue
<template>
  <UCheckbox required label="Check me" />
</template>
```

### Description

Use the `description` prop to set the description of the Checkbox.

```vue
<template>
  <UCheckbox label="Check me" description="This is a checkbox." />
</template>
```

### Icon

Use the `icon` prop to set the icon of the Checkbox when it is checked. Defaults to `i-lucide-check`.

```vue
<template>
  <UCheckbox icon="i-lucide-heart" default-value label="Check me" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.check\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.check\` key.

### Color

Use the `color` prop to change the color of the Checkbox.

```vue
<template>
  <UCheckbox color="neutral" default-value label="Check me" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the Checkbox.

```vue
<template>
  <UCheckbox color="primary" variant="card" default-value label="Check me" />
</template>
```

### Size

Use the `size` prop to change the size of the Checkbox.

```vue
<template>
  <UCheckbox size="xl" variant="list" default-value label="Check me" />
</template>
```

### Indicator

Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

```vue
<template>
  <UCheckbox indicator="end" variant="card" default-value label="Check me" />
</template>
```

### Disabled

Use the `disabled` prop to disable the Checkbox.

```vue
<template>
  <UCheckbox disabled label="Check me" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Checkbox component
 */
interface CheckboxProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  label?: string | undefined;
  description?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "card" | "list" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Position of the indicator.
   */
  indicator?: "start" | "end" | "hidden" | undefined;
  /**
   * The icon displayed when checked.
   */
  icon?: any;
  /**
   * The icon displayed when the checkbox is indeterminate.
   */
  indeterminateIcon?: any;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; base?: ClassNameValue; indicator?: ClassNameValue; icon?: ClassNameValue; wrapper?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the checkbox
   */
  disabled?: boolean | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * The value given as data when submitted with a `name`.
   */
  value?: AcceptableValue | undefined;
  /**
   * Id of the element
   */
  id?: string | undefined;
  /**
   * The value of the checkbox when it is initially rendered. Use when you do not need to control its value.
   */
  defaultValue?: T | "indeterminate" | undefined;
  /**
   * The controlled value of the checkbox. Can be binded with v-model.
   */
  modelValue?: T | "indeterminate" | null | undefined;
  /**
   * The value used when the checkbox is checked. Defaults to `true`.
   */
  trueValue?: T | undefined;
  /**
   * The value used when the checkbox is unchecked. Defaults to `false`.
   */
  falseValue?: T | undefined;
  autofocus?: Booleanish | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Checkbox component
 */
interface CheckboxSlots {
  label(): any;
  description(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Checkbox component
 */
interface CheckboxEmits {
  change: (payload: [event: Event]) => void;
  update:modelValue: (payload: [value: T | "indeterminate"]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    checkbox: {
      slots: {
        root: 'relative flex items-start',
        container: 'flex items-center',
        base: 'rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2',
        indicator: 'flex items-center justify-center size-full text-inverted',
        icon: 'shrink-0 size-full',
        wrapper: 'w-full',
        label: 'block font-medium text-default',
        description: 'text-muted'
      },
      variants: {
        color: {
          primary: {
            base: 'focus-visible:outline-primary',
            indicator: 'bg-primary'
          },
          secondary: {
            base: 'focus-visible:outline-secondary',
            indicator: 'bg-secondary'
          },
          success: {
            base: 'focus-visible:outline-success',
            indicator: 'bg-success'
          },
          info: {
            base: 'focus-visible:outline-info',
            indicator: 'bg-info'
          },
          warning: {
            base: 'focus-visible:outline-warning',
            indicator: 'bg-warning'
          },
          error: {
            base: 'focus-visible:outline-error',
            indicator: 'bg-error'
          },
          neutral: {
            base: 'focus-visible:outline-inverted',
            indicator: 'bg-inverted'
          }
        },
        variant: {
          list: {
            root: ''
          },
          card: {
            root: 'border border-muted rounded-lg'
          }
        },
        indicator: {
          start: {
            root: 'flex-row',
            wrapper: 'ms-2'
          },
          end: {
            root: 'flex-row-reverse',
            wrapper: 'me-2'
          },
          hidden: {
            base: 'sr-only',
            wrapper: 'text-center'
          }
        },
        size: {
          xs: {
            base: 'size-3',
            container: 'h-4',
            wrapper: 'text-xs'
          },
          sm: {
            base: 'size-3.5',
            container: 'h-4',
            wrapper: 'text-xs'
          },
          md: {
            base: 'size-4',
            container: 'h-5',
            wrapper: 'text-sm'
          },
          lg: {
            base: 'size-4.5',
            container: 'h-5',
            wrapper: 'text-sm'
          },
          xl: {
            base: 'size-5',
            container: 'h-6',
            wrapper: 'text-base'
          }
        },
        required: {
          true: {
            label: "after:content-['*'] after:ms-0.5 after:text-error"
          }
        },
        disabled: {
          true: {
            root: 'opacity-75',
            base: 'cursor-not-allowed',
            label: 'cursor-not-allowed',
            description: 'cursor-not-allowed'
          }
        },
        checked: {
          true: ''
        }
      },
      compoundVariants: [
        {
          size: 'xs',
          variant: 'card',
          class: {
            root: 'p-2.5'
          }
        },
        {
          size: 'sm',
          variant: 'card',
          class: {
            root: 'p-3'
          }
        },
        {
          size: 'md',
          variant: 'card',
          class: {
            root: 'p-3.5'
          }
        },
        {
          size: 'lg',
          variant: 'card',
          class: {
            root: 'p-4'
          }
        },
        {
          size: 'xl',
          variant: 'card',
          class: {
            root: 'p-4.5'
          }
        },
        {
          color: 'primary',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-secondary'
          }
        },
        {
          color: 'success',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-success'
          }
        },
        {
          color: 'info',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-info'
          }
        },
        {
          color: 'warning',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-warning'
          }
        },
        {
          color: 'error',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-error'
          }
        },
        {
          color: 'neutral',
          variant: 'card',
          class: {
            root: 'has-data-[state=checked]:border-inverted'
          }
        },
        {
          variant: 'card',
          disabled: true,
          class: {
            root: 'cursor-not-allowed'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'list',
        indicator: 'start'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Checkbox.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/checkbox.ts).


# CheckboxGroup

## Usage

Use the `v-model` directive to control the value of the CheckboxGroup or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup :items="items" />
</template>
```

### Items

Use the `items` prop as an array of strings or numbers:

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup :items="items" />
</template>
```

You can also pass an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`value?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#value-key)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, icon?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { CheckboxGroupItem } from '@nuxt/ui'

const items = ref<CheckboxGroupItem[]>([
  {
    label: 'System',
    description: 'This is the first option.',
    value: 'system',
  },
  {
    label: 'Light',
    description: 'This is the second option.',
    value: 'light',
  },
  {
    label: 'Dark',
    description: 'This is the third option.',
    value: 'dark',
  },
])
</script>

<template>
  <UCheckboxGroup :items="items" />
</template>
```

\> \[!CAUTION]
\> When using objects, you need to reference the \`value\` property of the object in the \`v-model\` directive or the \`default-value\` prop.

### Value Key

You can change the property that is used to set the value by using the `value-key` prop. Defaults to `value`.

```vue
<script setup lang="ts">
import type { CheckboxGroupItem } from '@nuxt/ui'

const items = ref<CheckboxGroupItem[]>([
  {
    label: 'System',
    description: 'This is the first option.',
    id: 'system',
  },
  {
    label: 'Light',
    description: 'This is the second option.',
    id: 'light',
  },
  {
    label: 'Dark',
    description: 'This is the third option.',
    id: 'dark',
  },
])
</script>

<template>
  <UCheckboxGroup value-key="id" :items="items" />
</template>
```

### Legend

Use the `legend` prop to set the legend of the CheckboxGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup legend="Theme" :items="items" />
</template>
```

### Color

Use the `color` prop to change the color of the CheckboxGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup color="neutral" :items="items" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the CheckboxGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup color="primary" variant="card" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the CheckboxGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup size="xl" variant="list" :items="items" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the CheckboxGroup. Defaults to `vertical`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup orientation="horizontal" variant="list" :items="items" />
</template>
```

### Indicator

Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup indicator="end" variant="card" :items="items" />
</template>
```

### Disabled

Use the `disabled` prop to disable the CheckboxGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <UCheckboxGroup disabled :items="items" />
</template>
```

## API

### Props

```ts
/**
 * Props for the CheckboxGroup component
 */
interface CheckboxGroupProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  legend?: string | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value.
   * @default "\"value\" as never"
   */
  valueKey?: VK | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  items?: T | undefined;
  /**
   * The controlled value of the CheckboxGroup. Can be bind as `v-model`.
   */
  modelValue?: GetItemValue<T, VK, undefined, NestedItem<T>>[] | undefined;
  /**
   * The value of the CheckboxGroup when initially rendered. Use when you do not need to control the state of the CheckboxGroup.
   */
  defaultValue?: GetItemValue<T, VK, undefined, NestedItem<T>>[] | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  variant?: "table" | "list" | "card" | undefined;
  /**
   * The orientation the checkbox buttons are laid out.
   * @default "\"vertical\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  ui?: ({ root?: ClassNameValue; fieldset?: ClassNameValue; legend?: ClassNameValue; item?: ClassNameValue; } & { root?: ClassNameValue; container?: ClassNameValue; base?: ClassNameValue; indicator?: ClassNameValue; icon?: ClassNameValue; wrapper?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; }) | undefined;
  /**
   * When `true`, prevents the user from interacting with the checkboxes
   */
  disabled?: boolean | undefined;
  /**
   * Whether keyboard navigation should loop around
   */
  loop?: boolean | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * Position of the indicator.
   */
  indicator?: "start" | "end" | "hidden" | undefined;
  /**
   * The icon displayed when checked.
   */
  icon?: any;
}
```

### Slots

```ts
/**
 * Slots for the CheckboxGroup component
 */
interface CheckboxGroupSlots {
  legend(): any;
  label(): any;
  description(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the CheckboxGroup component
 */
interface CheckboxGroupEmits {
  update:modelValue: (payload: [value: GetItemValue<T, VK, undefined, NestedItem<T>>[]]) => void;
  change: (payload: [event: Event]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    checkboxGroup: {
      slots: {
        root: 'relative',
        fieldset: 'flex gap-x-2',
        legend: 'mb-1 block font-medium text-default',
        item: ''
      },
      variants: {
        orientation: {
          horizontal: {
            fieldset: 'flex-row'
          },
          vertical: {
            fieldset: 'flex-col'
          }
        },
        color: {
          primary: {},
          secondary: {},
          success: {},
          info: {},
          warning: {},
          error: {},
          neutral: {}
        },
        variant: {
          list: {},
          card: {},
          table: {
            item: 'border border-muted'
          }
        },
        size: {
          xs: {
            fieldset: 'gap-y-0.5',
            legend: 'text-xs'
          },
          sm: {
            fieldset: 'gap-y-0.5',
            legend: 'text-xs'
          },
          md: {
            fieldset: 'gap-y-1',
            legend: 'text-sm'
          },
          lg: {
            fieldset: 'gap-y-1',
            legend: 'text-sm'
          },
          xl: {
            fieldset: 'gap-y-1.5',
            legend: 'text-base'
          }
        },
        required: {
          true: {
            legend: "after:content-['*'] after:ms-0.5 after:text-error"
          }
        },
        disabled: {
          true: {}
        }
      },
      compoundVariants: [
        {
          size: 'xs',
          variant: 'table',
          class: {
            item: 'p-2.5'
          }
        },
        {
          size: 'sm',
          variant: 'table',
          class: {
            item: 'p-3'
          }
        },
        {
          size: 'md',
          variant: 'table',
          class: {
            item: 'p-3.5'
          }
        },
        {
          size: 'lg',
          variant: 'table',
          class: {
            item: 'p-4'
          }
        },
        {
          size: 'xl',
          variant: 'table',
          class: {
            item: 'p-4.5'
          }
        },
        {
          orientation: 'horizontal',
          variant: 'table',
          class: {
            item: 'first-of-type:rounded-s-lg last-of-type:rounded-e-lg',
            fieldset: 'gap-0 -space-x-px'
          }
        },
        {
          orientation: 'vertical',
          variant: 'table',
          class: {
            item: 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
            fieldset: 'gap-0 -space-y-px'
          }
        },
        {
          color: 'primary',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-primary/10 has-data-[state=checked]:border-primary/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'secondary',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-secondary/10 has-data-[state=checked]:border-secondary/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'success',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-success/10 has-data-[state=checked]:border-success/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'info',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-info/10 has-data-[state=checked]:border-info/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'warning',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-warning/10 has-data-[state=checked]:border-warning/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'error',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-error/10 has-data-[state=checked]:border-error/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'neutral',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-elevated has-data-[state=checked]:border-inverted/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          variant: 'table',
          disabled: true,
          class: {
            item: 'cursor-not-allowed'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        variant: 'list',
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/CheckboxGroup.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/checkbox-group.ts).


# Chip

## Usage

Wrap any component with a Chip to display an indicator.

```vue
<template>
  <UChip>
    <UButton icon="i-lucide-mail" color="neutral" variant="subtle" />
  </UChip>
</template>
```

### Color

Use the `color` prop to change the color of the Chip.

```vue
<template>
  <UChip color="neutral">
    <UButton icon="i-lucide-mail" color="neutral" variant="subtle" />
  </UChip>
</template>
```

### Size

Use the `size` prop to change the size of the Chip.

```vue
<template>
  <UChip size="3xl">
    <UButton icon="i-lucide-mail" color="neutral" variant="subtle" />
  </UChip>
</template>
```

### Text

Use the `text` prop to set the text of the Chip.

```vue
<template>
  <UChip :text="5" size="3xl">
    <UButton icon="i-lucide-mail" color="neutral" variant="subtle" />
  </UChip>
</template>
```

### Position

Use the `position` prop to change the position of the Chip.

```vue
<template>
  <UChip position="bottom-left">
    <UButton icon="i-lucide-mail" color="neutral" variant="subtle" />
  </UChip>
</template>
```

### Inset

Use the `inset` prop to display the Chip inside the component. This is useful when dealing with rounded components.

```vue
<template>
  <UChip inset>
    <UAvatar src="https://github.com/benjamincanac.png" loading="lazy" />
  </UChip>
</template>
```

### Standalone

Use the `standalone` prop alongside the `inset` prop to display the Chip inline.

```vue
<template>
  <UChip standalone inset />
</template>
```

\> \[!NOTE]
\> It's used this way in the \[\`CommandPalette\`]\(/docs/components/command-palette), \[\`InputMenu\`]\(/docs/components/input-menu), \[\`Select\`]\(/docs/components/select) or \[\`SelectMenu\`]\(/docs/components/select-menu) components for example.

## Examples

### Control visibility

You can control the visibility of the Chip using the `show` prop.

```vue [ChipShowExample.vue]
<script setup lang="ts">
const statuses = ['online', 'away', 'busy', 'offline']
const status = ref(statuses[0])

const color = computed(() => status.value ? { online: 'success', away: 'warning', busy: 'error', offline: 'neutral' }[status.value] as any : 'online')

const show = computed(() => status.value !== 'offline')

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    if (status.value) {
      status.value = statuses[(statuses.indexOf(status.value) + 1) % statuses.length]
    }
  }, 1000)
})
</script>

<template>
  <UChip :color="color" :show="show" inset>
    <UAvatar src="https://github.com/benjamincanac.png" loading="lazy" />
  </UChip>
</template>
```

\> \[!NOTE]
\> In this example, the Chip has a color per status and is displayed when the status is not \`offline\`.

## API

### Props

```ts
/**
 * Props for the Chip component
 */
interface ChipProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Display some text inside the chip.
   */
  text?: string | number | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "3xs" | "2xs" | "2xl" | "3xl" | undefined;
  /**
   * The position of the chip.
   */
  position?: "top-right" | "bottom-right" | "top-left" | "bottom-left" | undefined;
  /**
   * When `true`, keep the chip inside the component for rounded elements.
   * @default "false"
   */
  inset?: boolean | undefined;
  /**
   * When `true`, render the chip relatively to the parent.
   * @default "false"
   */
  standalone?: boolean | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; } | undefined;
  /**
   * @default "true"
   */
  show?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Chip component
 */
interface ChipSlots {
  default(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Chip component
 */
interface ChipEmits {
  update:show: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    chip: {
      slots: {
        root: 'relative inline-flex items-center justify-center shrink-0',
        base: 'rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap'
      },
      variants: {
        color: {
          primary: 'bg-primary',
          secondary: 'bg-secondary',
          success: 'bg-success',
          info: 'bg-info',
          warning: 'bg-warning',
          error: 'bg-error',
          neutral: 'bg-inverted'
        },
        size: {
          '3xs': 'h-[4px] min-w-[4px] text-[4px]',
          '2xs': 'h-[5px] min-w-[5px] text-[5px]',
          xs: 'h-[6px] min-w-[6px] text-[6px]',
          sm: 'h-[7px] min-w-[7px] text-[7px]',
          md: 'h-[8px] min-w-[8px] text-[8px]',
          lg: 'h-[9px] min-w-[9px] text-[9px]',
          xl: 'h-[10px] min-w-[10px] text-[10px]',
          '2xl': 'h-[11px] min-w-[11px] text-[11px]',
          '3xl': 'h-[12px] min-w-[12px] text-[12px]'
        },
        position: {
          'top-right': 'top-0 right-0',
          'bottom-right': 'bottom-0 right-0',
          'top-left': 'top-0 left-0',
          'bottom-left': 'bottom-0 left-0'
        },
        inset: {
          false: ''
        },
        standalone: {
          false: 'absolute'
        }
      },
      compoundVariants: [
        {
          position: 'top-right',
          inset: false,
          class: '-translate-y-1/2 translate-x-1/2 transform'
        },
        {
          position: 'bottom-right',
          inset: false,
          class: 'translate-y-1/2 translate-x-1/2 transform'
        },
        {
          position: 'top-left',
          inset: false,
          class: '-translate-y-1/2 -translate-x-1/2 transform'
        },
        {
          position: 'bottom-left',
          inset: false,
          class: 'translate-y-1/2 -translate-x-1/2 transform'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        position: 'top-right'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Chip.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/chip.ts).


# Collapsible

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Collapsible.

Then, use the `#content` slot to add the content displayed when the Collapsible is open.

```vue
<template>
  <UCollapsible class="flex flex-col gap-2 w-48">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />
  
    <template #content>
      <Placeholder class="h-48" />
    </template></UCollapsible>
</template>
```

### Unmount

Use the `unmount-on-hide` prop to prevent the content from being unmounted when the Collapsible is collapsed. Defaults to `true`.

```vue
<template>
  <UCollapsible :unmount-on-hide="false" class="flex flex-col gap-2 w-48">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />
  
    <template #content>
      <Placeholder class="h-48" />
    </template></UCollapsible>
</template>
```

\> \[!NOTE]
\> You can inspect the DOM to see the content being rendered.

### Disabled

Use the `disabled` prop to disable the Collapsible.

```vue
<template>
  <UCollapsible class="flex flex-col gap-2 w-48" disabled>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />
  
    <template #content>
      <Placeholder class="h-48" />
    </template></UCollapsible>
</template>
```

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [CollapsibleOpenExample.vue]
<script setup lang="ts">
const open = ref(true)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UCollapsible v-model:open="open" class="flex flex-col gap-2 w-48">
    <UButton
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-down"
      block
    />

    <template #content>
      <Placeholder class="h-48" />
    </template>
  </UCollapsible>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Collapsible by pressing .

\> \[!TIP]
\> This allows you to move the trigger outside of the Collapsible or remove it entirely.

### With rotating icon

Here is an example with a rotating icon in the Button that indicates the open state of the Collapsible.

```vue [CollapsibleIconExample.vue]
<template>
  <UCollapsible class="flex flex-col gap-2 w-48">
    <UButton
      class="group"
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-down"
      :ui="{
        trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
      }"
      block
    />

    <template #content>
      <Placeholder class="h-48" />
    </template>
  </UCollapsible>
</template>
```

## API

### Props

```ts
/**
 * Props for the Collapsible component
 */
interface CollapsibleProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { root?: ClassNameValue; content?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the collapsible.
   */
  disabled?: boolean | undefined;
  /**
   * The open state of the collapsible when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the collapsible. Can be binded with `v-model`.
   */
  open?: boolean | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "true"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Collapsible component
 */
interface CollapsibleSlots {
  default(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Collapsible component
 */
interface CollapsibleEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    collapsible: {
      slots: {
        root: '',
        content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Collapsible.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/collapsible.ts).


# ColorModeAvatar

## Usage

The ColorModeAvatar component extends the [Avatar](https://ui.nuxt.com/docs/components/avatar) component, so you can pass any property such as `size`, `icon`, etc.

Use the `light` and `dark` props to define the source for light and dark mode.

```vue
<template>
  <UColorModeAvatar light="https://github.com/vuejs.png" dark="https://github.com/nuxt.png" />
</template>
```

\> \[!NOTE]
\> Switch between light and dark mode to see the different images:

## API

### Props

```ts
/**
 * Props for the ColorModeAvatar component
 */
interface ColorModeAvatarProps {
  light: string;
  dark: string;
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { root?: ClassNameValue; image?: ClassNameValue; fallback?: ClassNameValue; icon?: ClassNameValue; } | undefined;
  alt?: string | undefined;
  crossorigin?: "" | "anonymous" | "use-credentials" | undefined;
  decoding?: "async" | "auto" | "sync" | undefined;
  height?: Numberish | undefined;
  loading?: "lazy" | "eager" | undefined;
  referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
  sizes?: string | undefined;
  srcset?: string | undefined;
  usemap?: string | undefined;
  width?: Numberish | undefined;
  icon?: any;
  text?: string | undefined;
  size?: "md" | "3xs" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl" | undefined;
  chip?: boolean | ChipProps | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes
\> This component also supports all native \`\<img>\` HTML attributes.

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/color-mode/ColorModeAvatar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-mode/color-mode-avatar.ts).


# ColorModeButton

## Usage

The ColorModeButton component extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UColorModeButton />
</template>
```

\> \[!NOTE]
\> The button defaults to \`color="neutral"\` and \`variant="ghost"\`.

## Examples

### With custom icons

\*\*Nuxt:\*\*
Use the app.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
export default defineAppConfig({
ui: {
icons: {
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
\`\`\`
\*\*Vue:\*\*
Use the vite.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
icons: {
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
]
})
\`\`\`

### With fallback slot

As the button is wrapped in a [ClientOnly](https://nuxt.com/docs/api/components/client-only){rel="&#x22;nofollow&#x22;"} component, you can pass a `fallback` slot to display a placeholder while the component is loading.

```vue
<template>
  <UColorModeButton>
    <template #fallback>
      <UButton loading variant="ghost" color="neutral" />
    </template></UColorModeButton>
</template>
```

## API

### Props

```ts
/**
 * Props for the ColorModeButton component
 */
interface ColorModeButtonProps {
  /**
   * @default "\"neutral\""
   */
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * @default "\"ghost\""
   */
  variant?: "link" | "ghost" | "solid" | "outline" | "soft" | "subtle" | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  ui?: { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; } | undefined;
  name?: string | undefined;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  label?: string | undefined;
  activeColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  activeVariant?: "link" | "ghost" | "solid" | "outline" | "soft" | "subtle" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the ColorModeButton component
 */
interface ColorModeButtonSlots {
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/color-mode/ColorModeButton.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-mode/color-mode-button.ts).


# ColorModeImage

## Usage

The ColorModeImage component uses the `<NuxtImg>` component when [`@nuxt/image`](https://github.com/nuxt/image){rel="&#x22;nofollow&#x22;"} is installed, falling back to `img` otherwise.

```vue
<template>
  <UColorModeImage light="https://picsum.photos/id/29/400" dark="https://picsum.photos/id/46/400" :width="200" :height="200" />
</template>
```

\> \[!NOTE]
\> Switch between light and dark mode to see the different images:

## API

### Props

```ts
/**
 * Props for the ColorModeImage component
 */
interface ColorModeImageProps {
  dark: string;
  light: string;
  alt?: string | undefined;
  crossorigin?: "" | "anonymous" | "use-credentials" | undefined;
  decoding?: "async" | "auto" | "sync" | undefined;
  height?: Numberish | undefined;
  loading?: "lazy" | "eager" | undefined;
  referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
  sizes?: string | undefined;
  srcset?: string | undefined;
  usemap?: string | undefined;
  width?: Numberish | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes
\> This component also supports all native \`\<img>\` HTML attributes.

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/color-mode/ColorModeImage.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-mode/color-mode-image.ts).


# ColorModeSelect

## Usage

The ColorModeSelect component extends the [SelectMenu](https://ui.nuxt.com/docs/components/select-menu) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UColorModeSelect />
</template>
```

## Examples

### With custom icons

\*\*Nuxt:\*\*
Use the app.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
export default defineAppConfig({
ui: {
icons: {
system: 'i-ph-desktop',
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
\`\`\`
\*\*Vue:\*\*
Use the vite.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
icons: {
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
]
})
\`\`\`

## API

### Props

```ts
/**
 * Props for the ColorModeSelect component
 */
interface ColorModeSelectProps {
  /**
   * The open state of the combobox when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the Combobox. Can be binded with `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; value?: ClassNameValue; placeholder?: ClassNameValue; arrow?: ClassNameValue; content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; input?: ClassNameValue; focusScope?: ClassNameValue; trailingClear?: ClassNameValue; } | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * The content of the menu.
   */
  content?: (Omit<ComboboxContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DismissableLayerEmits>>) | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  variant?: "ghost" | "outline" | "soft" | "subtle" | "none" | undefined;
  autofocus?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with listbox
   */
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * Whether to reset the searchTerm when the Combobox input blurred
   */
  resetSearchTermOnBlur?: boolean | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox value is selected
   */
  resetSearchTermOnSelect?: boolean | undefined;
  /**
   * When `true` the `modelValue` will be reset to `null` (or `[]` if `multiple`)
   */
  resetModelValueOnClear?: boolean | undefined;
  /**
   * When `true`, hover over item will trigger highlight
   */
  highlightOnHover?: boolean | undefined;
  /**
   * Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
   */
  by?: string | ((a: SelectMenuItem[], b: SelectMenuItem[]) => boolean) | undefined;
  /**
   * The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu.
   */
  defaultValue?: SelectMenuItem | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: false | undefined;
  required?: boolean | undefined;
  id?: string | undefined;
  /**
   * The placeholder text when the select is empty.
   */
  placeholder?: string | undefined;
  /**
   * Whether to display the search input or not.
   * Can be an object to pass additional props to the input.
   * `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
   * @default "false"
   */
  searchInput?: boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * Display a clear button to reset the model value.
   * Can be an object to pass additional props to the Button.
   */
  clear?: false | (false & Partial<Omit<ButtonProps, LinkPropsKeys>>) | undefined;
  /**
   * The icon displayed in the clear button.
   */
  clearIcon?: any;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<ComboboxArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the menu in a portal.
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, all groups are flattened into a single list due to a limitation of Reka UI (https://github.com/unovue/reka-ui/issues/1885).
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value instead of the object itself.
   */
  valueKey?: undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   */
  labelKey?: string | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   */
  descriptionKey?: string | undefined;
  modelModifiers?: Omit<ModelModifiers, "lazy"> | undefined;
  /**
   * Determines if custom user input that does not exist in options can be added.
   */
  createItem?: boolean | "always" | { position?: "top" | "bottom" | undefined; when?: "empty" | "always" | undefined; } | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
   */
  ignoreFilter?: boolean | undefined;
  autofocusDelay?: number | undefined;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/color-mode/ColorModeSelect.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-mode/color-mode-select.ts).


# ColorModeSwitch

## Usage

The ColorModeSwitch component extends the [Switch](https://ui.nuxt.com/docs/components/switch) component, so you can pass any property such as `color`, `size`, etc.

```vue
<template>
  <UColorModeSwitch />
</template>
```

## Examples

### With custom icons

\*\*Nuxt:\*\*
Use the app.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
export default defineAppConfig({
ui: {
icons: {
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
\`\`\`
\*\*Vue:\*\*
Use the vite.config.ts to customize the icon with the ui.icons property:
\`\`\`ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: \[
vue(),
ui({
ui: {
icons: {
light: 'i-ph-sun',
dark: 'i-ph-moon'
}
}
})
]
})
\`\`\`

## API

### Props

```ts
/**
 * Props for the ColorModeSwitch component
 */
interface ColorModeSwitchProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; container?: ClassNameValue; thumb?: ClassNameValue; icon?: ClassNameValue; wrapper?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; } | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  autofocus?: Booleanish | undefined;
  /**
   * When `true`, prevents the user from interacting with the switch.
   */
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  label?: string | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * The state of the switch when it is initially rendered. Use when you do not need to control its state.
   */
  defaultValue?: boolean | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  id?: string | undefined;
  /**
   * The value given as data when submitted with a `name`.
   */
  value?: string | undefined;
  description?: string | undefined;
  /**
   * The value used when the switch is on. Defaults to `true`.
   */
  trueValue?: boolean | undefined;
  /**
   * The value used when the switch is off. Defaults to `false`.
   */
  falseValue?: boolean | undefined;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/color-mode/ColorModeSwitch.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-mode/color-mode-switch.ts).


# ColorPicker

## Usage

Use the `v-model` directive to control the value of the ColorPicker.

```vue
<template>
  <UColorPicker model-value="#00C16A" />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UColorPicker default-value="#00BCD4" />
</template>
```

### RGB Format

Use the `format` prop to set `rgb` value of the ColorPicker.

```vue
<template>
  <UColorPicker format="rgb" model-value="rgb(0, 193, 106)" />
</template>
```

### HSL Format

Use the `format` prop to set `hsl` value of the ColorPicker.

```vue
<template>
  <UColorPicker format="hsl" model-value="hsl(153, 100%, 37.8%)" />
</template>
```

### CMYK Format

Use the `format` prop to set `cmyk` value of the ColorPicker.

```vue
<template>
  <UColorPicker format="cmyk" model-value="cmyk(100%, 0%, 45.08%, 24.31%)" />
</template>
```

### CIELab Format

Use the `format` prop to set `lab` value of the ColorPicker.

```vue
<template>
  <UColorPicker format="lab" model-value="lab(68.88% -60.41% 32.55%)" />
</template>
```

### Throttle

Use the `throttle` prop to set the throttle value of the ColorPicker.

```vue
<template>
  <UColorPicker :throttle="100" model-value="#00C16A" />
</template>
```

### Size

Use the `size` prop to set the size of the ColorPicker.

```vue
<template>
  <UColorPicker size="xl" />
</template>
```

### Disabled

Use the `disabled` prop to disable the ColorPicker.

```vue
<template>
  <UColorPicker disabled />
</template>
```

## Examples

### As a color chooser

Use a [Button](https://ui.nuxt.com/docs/components/button) and a [Popover](https://ui.nuxt.com/docs/components/popover) component to create a color chooser.

```vue [ColorPickerChooserExample.vue]
<script setup lang="ts">
const color = ref('#00C16A')

const chip = computed(() => ({ backgroundColor: color.value }))
</script>

<template>
  <UPopover>
    <UButton label="Choose color" color="neutral" variant="outline">
      <template #leading>
        <span :style="chip" class="size-3 rounded-full" />
      </template>
    </UButton>

    <template #content>
      <UColorPicker v-model="color" class="p-2" />
    </template>
  </UPopover>
</template>
```

## API

### Props

```ts
/**
 * Props for the ColorPicker component
 */
interface ColorPickerProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Throttle time in ms for the color picker
   * @default "50"
   */
  throttle?: number | undefined;
  /**
   * Disable the color picker
   */
  disabled?: boolean | undefined;
  /**
   * The default value of the color picker
   * @default "\"#FFFFFF\""
   */
  defaultValue?: string | undefined;
  /**
   * Format of the color
   * @default "\"hex\""
   */
  format?: "hex" | "rgb" | "hsl" | "cmyk" | "lab" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  ui?: { root?: ClassNameValue; picker?: ClassNameValue; selector?: ClassNameValue; selectorBackground?: ClassNameValue; selectorThumb?: ClassNameValue; track?: ClassNameValue; trackThumb?: ClassNameValue; } | undefined;
  modelValue?: string | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the ColorPicker component
 */
interface ColorPickerEmits {
  update:modelValue: (payload: [value: string | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    colorPicker: {
      slots: {
        root: 'data-[disabled]:opacity-75',
        picker: 'flex gap-4',
        selector: 'rounded-md touch-none',
        selectorBackground: 'w-full h-full relative rounded-md',
        selectorThumb: '-translate-y-1/2 -translate-x-1/2 absolute size-4 ring-2 ring-white rounded-full cursor-pointer data-[disabled]:cursor-not-allowed',
        track: 'w-[8px] relative rounded-md touch-none',
        trackThumb: 'absolute transform -translate-y-1/2 -translate-x-[4px] rtl:translate-x-[4px] size-4 rounded-full ring-2 ring-white cursor-pointer data-[disabled]:cursor-not-allowed'
      },
      variants: {
        size: {
          xs: {
            selector: 'w-38 h-38',
            track: 'h-38'
          },
          sm: {
            selector: 'w-40 h-40',
            track: 'h-40'
          },
          md: {
            selector: 'w-42 h-42',
            track: 'h-42'
          },
          lg: {
            selector: 'w-44 h-44',
            track: 'h-44'
          },
          xl: {
            selector: 'w-46 h-46',
            track: 'h-46'
          }
        }
      },
      compoundVariants: [],
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ColorPicker.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/color-picker.ts).


# CommandPalette

## Usage

Use the `v-model` directive to control the value of the CommandPalette or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette class="flex-1 h-80" />
</template>
```

\> \[!TIP]
\> See: #control-selected-items
\> You can also use the \`@update\:model-value\` event to listen to the selected item(s).

### Groups

The CommandPalette component filters groups and ranks matching commands by relevance as users type. It provides dynamic, instant search results for efficient command discovery. Use the `groups` prop as an array of objects with the following properties:

- `id: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `items?: CommandPaletteItem[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`ignoreFilter?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-ignore-filter)
- [`postFilter?: (searchTerm: string, items: T[]) => T[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-post-filtered-items)
- `highlightedIcon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

\> \[!CAUTION]
\> You must provide an \`id\` for each group otherwise the group will be ignored.

Each group contains an `items` array of objects that define the commands. Each item can have the following properties:

- `prefix?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `suffix?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `chip?: ChipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `kbds?: string[] | KbdProps[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `active?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `loading?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `placeholder?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `children?: CommandPaletteItem[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette class="flex-1" />
</template>
```

\> \[!TIP]
\> See: #with-children-in-items
\> Each item can take a \`children\` array of objects with the following properties to create submenus:

### Multiple

Use the `multiple` prop to allow multiple selections.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette multiple class="flex-1" />
</template>
```

\> \[!CAUTION]
\> Ensure to pass an array to the \`default-value\` prop or the \`v-model\` directive.

### Placeholder

Use the `placeholder` prop to change the placeholder text.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette placeholder="Search an app..." class="flex-1" />
</template>
```

### Size `4.4+`

Use the `size` prop to change the size of the CommandPalette.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette size="xl" class="flex-1" />
</template>
```

### Icon

Use the `icon` prop to customize the input [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-search`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette icon="i-lucide-box" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.search\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.search\` key.

### Selected Icon

Use the `selected-icon` prop to customize the selected item [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-check`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette multiple selected-icon="i-lucide-circle-check" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.check\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.check\` key.

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon) when an item has children. Defaults to `i-lucide-chevron-right`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette trailing-icon="i-lucide-arrow-right" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronRight\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronRight\` key.

### Loading

Use the `loading` prop to show a loading icon on the CommandPalette.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette loading class="flex-1" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette loading loading-icon="i-lucide-loader" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Close

Use the `close` prop to display a [Button](https://ui.nuxt.com/docs/components/button) to dismiss the CommandPalette.

\> \[!TIP]
\> An \`update\:open\` event will be emitted when the close button is clicked.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette close class="flex-1" />
</template>
```

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette class="flex-1" />
</template>
```

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette close close-icon="i-lucide-arrow-right" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Back

Use the `back` prop to customize or hide the back button (with `false` value) displayed when navigating into a submenu.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette class="flex-1" />
</template>
```

### Back Icon

Use the `back-icon` prop to customize the back button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-arrow-left`.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette back back-icon="i-lucide-house" class="flex-1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.arrowLeft\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.arrowLeft\` key.

### Disabled

Use the `disabled` prop to disable the CommandPalette.

```vue
<script setup lang="ts">
import type { CommandPaletteGroup } from '@nuxt/ui'
</script>

<template>
  <UCommandPalette disabled class="flex-1" />
</template>
```

## Examples

### Control selected item(s)

You can control the selected item(s) by using the `default-value` prop or the `v-model` directive, by using the `onSelect` field on each item or by using the `@update:model-value` event.

```vue [CommandPaletteSelectExample.vue]
<script setup lang="ts">
const toast = useToast()

const groups = ref([
  {
    id: 'users',
    label: 'Users',
    items: [
      {
        label: 'Benjamin Canac',
        suffix: 'benjamincanac',
        to: 'https://github.com/benjamincanac',
        target: '_blank',
        avatar: {
          src: 'https://github.com/benjamincanac.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        to: 'https://github.com/romhml',
        target: '_blank',
        avatar: {
          src: 'https://github.com/romhml.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        to: 'https://github.com/atinux',
        target: '_blank',
        avatar: {
          src: 'https://github.com/atinux.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Hugo Richard',
        suffix: 'HugoRCD',
        to: 'https://github.com/HugoRCD',
        target: '_blank',
        avatar: {
          src: 'https://github.com/HugoRCD.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Sandro Circi',
        suffix: 'sandros94',
        to: 'https://github.com/sandros94',
        target: '_blank',
        avatar: {
          src: 'https://github.com/sandros94.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        to: 'https://github.com/danielroe',
        target: '_blank',
        avatar: {
          src: 'https://github.com/danielroe.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Jakub Michálek',
        suffix: 'J-Michalek',
        to: 'https://github.com/J-Michalek',
        target: '_blank',
        avatar: {
          src: 'https://github.com/J-Michalek.png',
          loading: 'lazy' as const
        }
      },
      {
        label: 'Eugen Istoc',
        suffix: 'genu',
        to: 'https://github.com/genu',
        target: '_blank',
        avatar: {
          src: 'https://github.com/genu.png',
          loading: 'lazy' as const
        }
      }
    ]
  },
  {
    id: 'actions',
    items: [
      {
        label: 'Add new file',
        suffix: 'Create a new file in the current directory or workspace.',
        icon: 'i-lucide-file-plus',
        kbds: [
          'meta',
          'N'
        ],
        onSelect() {
          toast.add({ title: 'Add new file' })
        }
      },
      {
        label: 'Add new folder',
        suffix: 'Create a new folder in the current directory or workspace.',
        icon: 'i-lucide-folder-plus',
        kbds: [
          'meta',
          'F'
        ],
        onSelect() {
          toast.add({ title: 'Add new folder' })
        }
      },
      {
        label: 'Add hashtag',
        suffix: 'Add a hashtag to the current item.',
        icon: 'i-lucide-hash',
        kbds: [
          'meta',
          'H'
        ],
        onSelect() {
          toast.add({ title: 'Add hashtag' })
        }
      },
      {
        label: 'Add label',
        suffix: 'Add a label to the current item.',
        icon: 'i-lucide-tag',
        kbds: [
          'meta',
          'L'
        ],
        onSelect() {
          toast.add({ title: 'Add label' })
        }
      }
    ]
  }
])

function onSelect(item: any) {
  console.log(item)
}
</script>

<template>
  <UCommandPalette
    :groups="groups"
    class="flex-1 h-80"
    @update:model-value="onSelect"
  />
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to select a field of an item to use as the value instead of the object itself. Use the \`by\` prop to compare objects by a field instead of reference.

### Control search term

Use the `v-model:search-term` directive to control the search term.

```vue [CommandPaletteSearchTermExample.vue]
<script setup lang="ts">
const users = [
  {
    label: 'Benjamin Canac',
    suffix: 'benjamincanac',
    to: 'https://github.com/benjamincanac',
    target: '_blank',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Romain Hamel',
    suffix: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Sébastien Chopin',
    suffix: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Hugo Richard',
    suffix: 'HugoRCD',
    to: 'https://github.com/HugoRCD',
    target: '_blank',
    avatar: {
      src: 'https://github.com/HugoRCD.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Sandro Circi',
    suffix: 'sandros94',
    to: 'https://github.com/sandros94',
    target: '_blank',
    avatar: {
      src: 'https://github.com/sandros94.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Daniel Roe',
    suffix: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Jakub Michálek',
    suffix: 'J-Michalek',
    to: 'https://github.com/J-Michalek',
    target: '_blank',
    avatar: {
      src: 'https://github.com/J-Michalek.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Eugen Istoc',
    suffix: 'genu',
    to: 'https://github.com/genu',
    target: '_blank',
    avatar: {
      src: 'https://github.com/genu.png',
      loading: 'lazy' as const
    }
  }
]

const searchTerm = ref('B')

function onSelect() {
  searchTerm.value = ''
}
</script>

<template>
  <UCommandPalette
    v-model:search-term="searchTerm"
    :groups="[{ id: 'users', items: users }]"
    class="flex-1"
    @update:model-value="onSelect"
  />
</template>
```

\> \[!NOTE]
\> This example uses the \`@update\:model-value\` event to reset the search term when an item is selected.

### With children in items

You can create hierarchical menus by using the `children` property in items. When an item has children, it will automatically display a chevron icon and enable navigation into a submenu.

```vue [CommandPaletteItemsChildrenExample.vue]
<script setup lang="ts">
const toast = useToast()

const groups = [{
  id: 'actions',
  label: 'Actions',
  items: [{
    label: 'Create new',
    icon: 'i-lucide-plus',
    children: [{
      label: 'New file',
      icon: 'i-lucide-file-plus',
      suffix: 'Create a new file in the current directory',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'New file created!' })
      },
      kbds: ['meta', 'N']
    }, {
      label: 'New folder',
      icon: 'i-lucide-folder-plus',
      suffix: 'Create a new folder in the current directory',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'New folder created!' })
      },
      kbds: ['meta', 'F']
    }, {
      label: 'New project',
      icon: 'i-lucide-folder-git',
      suffix: 'Create a new project from a template',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'New project created!' })
      },
      kbds: ['meta', 'P']
    }]
  }, {
    label: 'Share',
    icon: 'i-lucide-share',
    children: [{
      label: 'Copy link',
      icon: 'i-lucide-link',
      suffix: 'Copy a link to the current item',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'Link copied to clipboard!' })
      },
      kbds: ['meta', 'L']
    }, {
      label: 'Share via email',
      icon: 'i-lucide-mail',
      suffix: 'Share the current item via email',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'Share via email dialog opened!' })
      }
    }, {
      label: 'Share on social',
      icon: 'i-lucide-share-2',
      suffix: 'Share the current item on social media',
      children: [{
        label: 'Twitter',
        icon: 'i-simple-icons-twitter',
        onSelect(e: Event) {
          e.preventDefault()
          toast.add({ title: 'Shared on Twitter!' })
        }
      }, {
        label: 'LinkedIn',
        icon: 'i-simple-icons-linkedin',
        onSelect(e: Event) {
          e.preventDefault()
          toast.add({ title: 'Shared on LinkedIn!' })
        }
      }, {
        label: 'Facebook',
        icon: 'i-simple-icons-facebook',
        onSelect(e: Event) {
          e.preventDefault()
          toast.add({ title: 'Shared on Facebook!' })
        }
      }]
    }]
  }, {
    label: 'Settings',
    icon: 'i-lucide-settings',
    children: [{
      label: 'General',
      icon: 'i-lucide-sliders',
      suffix: 'Configure general settings',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'General settings opened!' })
      }
    }, {
      label: 'Appearance',
      icon: 'i-lucide-palette',
      suffix: 'Customize the appearance',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'Appearance settings opened!' })
      }
    }, {
      label: 'Security',
      icon: 'i-lucide-shield',
      suffix: 'Manage security settings',
      onSelect(e: Event) {
        e.preventDefault()
        toast.add({ title: 'Security settings opened!' })
      }
    }]
  }]
}]
</script>

<template>
  <UCommandPalette :groups="groups" class="flex-1" />
</template>
```

\> \[!NOTE]
\> When navigating into a submenu\:The search term is resetA back button appears in the inputYou can go back to the previous group by pressing the key

### With fetched items

You can fetch items from an API and use them in the CommandPalette.

```vue [CommandPaletteFetchExample.vue]
<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status } = useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  server: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || []
}])
</script>

<template>
  <UCommandPalette
    v-model:search-term="searchTerm"
    :loading="status === 'pending' || status === 'idle'"
    :groups="groups"
    class="flex-1 h-80"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`server: false\` to fetch data on the client without blocking the initial render. The loading state checks for both \`pending\` and \`idle\` status to display a loading indicator before and during the fetch.

### With ignore filter

You can set the `ignoreFilter` field to `true` on a group to disable the internal search and use your own search logic.

```vue [CommandPaletteIgnoreFilterExample.vue]
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: users, status } = useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users-search',
  params: { q: searchTermDebounced },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  server: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])
</script>

<template>
  <UCommandPalette
    v-model:search-term="searchTerm"
    :loading="status === 'pending' || status === 'idle'"
    :groups="groups"
    class="flex-1 h-80"
  />
</template>
```

\> \[!NOTE]
\> This example uses \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/#refdebounced) to debounce the API calls. The loading state checks for both \`pending\` and \`idle\` status to display a loading indicator before and during the fetch.

### With post-filtered items

You can use the `postFilter` field on a group to filter items after the search happened.

```vue [CommandPalettePostFilterExample.vue]
<script setup lang="ts">
const items = [
  {
    id: '/',
    label: 'Introduction',
    level: 1
  },
  {
    id: '/docs/getting-started#whats-new-in-v3',
    label: 'What\'s new in v3?',
    level: 2
  },
  {
    id: '/docs/getting-started#reka-ui',
    label: 'Reka UI',
    level: 3
  },
  {
    id: '/docs/getting-started#tailwind-css',
    label: 'Tailwind CSS',
    level: 3
  },
  {
    id: '/docs/getting-started#tailwind-variants',
    label: 'Tailwind Variants',
    level: 3
  },
  {
    id: '/docs/getting-started/installation',
    label: 'Installation',
    level: 1
  }
]

function postFilter(searchTerm: string, items: any[]) {
  // Filter only first level items if no searchTerm
  if (!searchTerm) {
    return items?.filter(item => item.level === 1)
  }

  return items
}
</script>

<template>
  <UCommandPalette :groups="[{ id: 'files', items, postFilter }]" class="flex-1" />
</template>
```

\> \[!NOTE]
\> Start typing to see items with higher level appear.

### With custom fuse search

You can use the `fuse` prop to override the options of [useFuse](https://vueuse.org/integrations/useFuse){rel="&#x22;nofollow&#x22;"} which defaults to:

```ts
{
  fuseOptions: {
    ignoreLocation: true,
    threshold: 0.1,
    keys: ['label', 'suffix']
  },
  resultLimit: 12,
  matchAllWhenSearchEmpty: true
}
```

\> \[!TIP]
\> The \`fuseOptions\` are the options of \[Fuse.js]\(https\://www\.fusejs.io/api/options.html), the \`resultLimit\` is the maximum number of results to return and the \`matchAllWhenSearchEmpty\` is a boolean to match all items when the search term is empty.

You can for example set `{ fuseOptions: { includeMatches: true } }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} to highlight the search term in the items.

```vue [CommandPaletteFuseExample.vue]
<script setup lang="ts">
const { data: users } = useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  server: false
})
</script>

<template>
  <UCommandPalette
    :groups="[{ id: 'users', items: users || [] }]"
    :fuse="{ fuseOptions: { includeMatches: true } }"
    class="flex-1 h-80"
  />
</template>
```

### With virtualization `4.1+`

Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.

\> \[!WARNING]
\> See: https\://github.com/unovue/reka-ui/issues/1885
\> When enabled, all groups are flattened into a single list due to a limitation of Reka UI.

```vue [CommandPaletteVirtualizeExample.vue]
<script setup lang="ts">
import type { CommandPaletteItem } from '@nuxt/ui'

const items: CommandPaletteItem[] = Array(1000)
  .fill(0)
  .map((_, value) => ({
    label: `item-${value}`,
    value
  }))

const groups = [
  {
    id: 'items',
    items
  }
]
</script>

<template>
  <UCommandPalette
    virtualize
    :fuse="{ resultLimit: 1000 }"
    :groups="groups"
    class="flex-1 h-80"
  />
</template>
```

### Within a Popover

You can use the CommandPalette component inside a [Popover](https://ui.nuxt.com/docs/components/popover)'s content.

```vue [PopoverCommandPaletteExample.vue]
<script setup lang="ts">
import type { CommandPaletteItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error'
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success'
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info'
    }
  }
] satisfies CommandPaletteItem[])

const label = ref([])
</script>

<template>
  <UPopover :content="{ side: 'right', align: 'start' }">
    <UButton
      icon="i-lucide-tag"
      label="Select labels"
      color="neutral"
      variant="subtle"
    />

    <template #content>
      <UCommandPalette
        v-model="label"
        multiple
        placeholder="Search labels..."
        :groups="[{ id: 'labels', items }]"
        :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }"
      />
    </template>
  </UPopover>
</template>
```

### Within a Modal

You can use the CommandPalette component inside a [Modal](https://ui.nuxt.com/docs/components/modal)'s content.

```vue [ModalCommandPaletteExample.vue]
<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'modal-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UModal @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the Modal opens.

### Within a Drawer

You can use the CommandPalette component inside a [Drawer](https://ui.nuxt.com/docs/components/drawer)'s content.

```vue [DrawerCommandPaletteExample.vue]
<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'drawer-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UDrawer :handle="false" @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UDrawer>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the Drawer opens.

### Listen open state

When using the `close` prop, you can listen to the `update:open` event when the button is clicked.

```vue [CommandPaletteOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

const users = [
  {
    label: 'Benjamin Canac',
    suffix: 'benjamincanac',
    to: 'https://github.com/benjamincanac',
    target: '_blank',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Romain Hamel',
    suffix: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Sébastien Chopin',
    suffix: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Hugo Richard',
    suffix: 'HugoRCD',
    to: 'https://github.com/HugoRCD',
    target: '_blank',
    avatar: {
      src: 'https://github.com/HugoRCD.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Sandro Circi',
    suffix: 'sandros94',
    to: 'https://github.com/sandros94',
    target: '_blank',
    avatar: {
      src: 'https://github.com/sandros94.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Daniel Roe',
    suffix: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Jakub Michálek',
    suffix: 'J-Michalek',
    to: 'https://github.com/J-Michalek',
    target: '_blank',
    avatar: {
      src: 'https://github.com/J-Michalek.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Eugen Istoc',
    suffix: 'genu',
    to: 'https://github.com/genu',
    target: '_blank',
    avatar: {
      src: 'https://github.com/genu.png',
      loading: 'lazy' as const
    }
  }
]
</script>

<template>
  <UModal v-model:open="open">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette close :groups="[{ id: 'users', items: users }]" @update:open="open = $event" />
    </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> This can be useful when using the CommandPalette inside a \[\`Modal\`]\(/docs/components/modal) for example.

### With footer slot

Use the `#footer` slot to add custom content at the bottom of the CommandPalette, such as keyboard shortcuts help or additional actions.

```vue [CommandPaletteFooterSlotExample.vue]
<script setup lang="ts">
const groups = [
  {
    id: 'actions',
    items: [
      {
        label: 'Add new file',
        suffix: 'Create a new file in the current directory',
        icon: 'i-lucide-file-plus',
        kbds: ['meta', 'N']
      },
      {
        label: 'Add new folder',
        suffix: 'Create a new folder in the current directory',
        icon: 'i-lucide-folder-plus',
        kbds: ['meta', 'F']
      },
      {
        label: 'Search files',
        suffix: 'Search across all files in the project',
        icon: 'i-lucide-search',
        kbds: ['meta', 'P']
      },
      {
        label: 'Settings',
        suffix: 'Open application settings',
        icon: 'i-lucide-settings',
        kbds: ['meta', ',']
      }
    ]
  },
  {
    id: 'recent',
    label: 'Recent',
    items: [
      {
        label: 'project.vue',
        suffix: 'components/',
        icon: 'i-vscode-icons-file-type-vue'
      },
      {
        label: 'readme.md',
        suffix: 'docs/',
        icon: 'i-vscode-icons-file-type-markdown'
      },
      {
        label: 'package.json',
        suffix: 'root/',
        icon: 'i-vscode-icons-file-type-node'
      }
    ]
  }
]
</script>

<template>
  <UCommandPalette :groups="groups" class="flex-1 h-80">
    <template #footer>
      <div class="flex items-center justify-between gap-2">
        <UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
        <div class="flex items-center gap-1">
          <UButton color="neutral" variant="ghost" label="Open Command" class="text-dimmed" size="xs">
            <template #trailing>
              <UKbd value="enter" />
            </template>
          </UButton>
          <USeparator orientation="vertical" class="h-4" />
          <UButton color="neutral" variant="ghost" label="Actions" class="text-dimmed" size="xs">
            <template #trailing>
              <UKbd value="meta" />
              <UKbd value="k" />
            </template>
          </UButton>
        </div>
      </div>
    </template>
  </UCommandPalette>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item or group.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ group.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ group.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ group.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ group.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [CommandPaletteCustomSlotExample.vue]
<script setup lang="ts">
const groups = [
  {
    id: 'settings',
    items: [
      {
        label: 'Profile',
        icon: 'i-lucide-user',
        kbds: ['meta', 'P']
      },
      {
        label: 'Billing',
        icon: 'i-lucide-credit-card',
        kbds: ['meta', 'B'],
        slot: 'billing' as const
      },
      {
        label: 'Notifications',
        icon: 'i-lucide-bell'
      },
      {
        label: 'Security',
        icon: 'i-lucide-lock'
      }
    ]
  },
  {
    id: 'users',
    label: 'Users',
    slot: 'users' as const,
    items: [
      {
        label: 'Benjamin Canac',
        suffix: 'benjamincanac',
        to: 'https://github.com/benjamincanac',
        target: '_blank'
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        to: 'https://github.com/romhml',
        target: '_blank'
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        to: 'https://github.com/atinux',
        target: '_blank'
      },
      {
        label: 'Hugo Richard',
        suffix: 'HugoRCD',
        to: 'https://github.com/HugoRCD',
        target: '_blank'
      },
      {
        label: 'Sandro Circi',
        suffix: 'sandros94',
        to: 'https://github.com/sandros94',
        target: '_blank'
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        to: 'https://github.com/danielroe',
        target: '_blank'
      },
      {
        label: 'Jakub Michálek',
        suffix: 'J-Michalek',
        to: 'https://github.com/J-Michalek',
        target: '_blank'
      },
      {
        label: 'Eugen Istoc',
        suffix: 'genu',
        to: 'https://github.com/genu',
        target: '_blank'
      }
    ]
  }
]
</script>

<template>
  <UCommandPalette :groups="groups" class="flex-1 h-80">
    <template #users-leading="{ item }">
      <UAvatar :src="`https://github.com/${item.suffix}.png`" size="2xs" loading="lazy" />
    </template>

    <template #billing-label="{ item }">
      <span class="font-medium text-primary">{{ item.label }}</span>

      <UBadge variant="subtle" size="sm">
        50% off
      </UBadge>
    </template>
  </UCommandPalette>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can also use the \`#item\`, \`#item-leading\`, \`#item-label\` and \`#item-trailing\` slots to customize all items.

## API

### Props

```ts
/**
 * Props for the CommandPalette component
 */
interface CommandPaletteProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  /**
   * The icon displayed in the input.
   */
  icon?: any;
  /**
   * The icon displayed on the right side of the input.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * The icon displayed when an item has children.
   */
  childrenIcon?: any;
  /**
   * The placeholder text for the input.
   */
  placeholder?: string | undefined;
  /**
   * Automatically focus the input when component is mounted.
   * @default "true"
   */
  autofocus?: boolean | undefined;
  /**
   * Display a close button in the input (useful when inside a Modal for example).
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * Display a button to navigate back in history.
   * `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
   * @default "true"
   */
  back?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the back button.
   */
  backIcon?: any;
  /**
   * Configure the input or hide it with `false`.
   * @default "true"
   */
  input?: boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  groups?: G[] | undefined;
  /**
   * Options for [useFuse](https://vueuse.org/integrations/useFuse).
   */
  fuse?: n<T> | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, all groups are flattened into a single list due to a limitation of Reka UI (https://github.com/unovue/reka-ui/issues/1885).
   * @default "false"
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value instead of the object itself.
   */
  valueKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the description from the item.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  /**
   * Whether to preserve the order of groups as defined in the `groups` prop when filtering.
   * When `false`, groups will appear based on item matches.
   * @default "false"
   */
  preserveGroupOrder?: boolean | undefined;
  ui?: { root?: ClassNameValue; input?: ClassNameValue; close?: ClassNameValue; back?: ClassNameValue; content?: ClassNameValue; footer?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingHighlightedIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelBase?: ClassNameValue; itemLabelPrefix?: ClassNameValue; itemLabelSuffix?: ClassNameValue; } | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with listbox
   */
  disabled?: boolean | undefined;
  /**
   * The controlled value of the listbox. Can be binded with `v-model`.
   */
  modelValue?: AcceptableValue | AcceptableValue[] | undefined;
  /**
   * The value of the listbox when initially rendered. Use when you do not need to control the state of the Listbox
   */
  defaultValue?: AcceptableValue | AcceptableValue[] | undefined;
  /**
   * When `true`, hover over item will trigger highlight
   * @default "true"
   */
  highlightOnHover?: boolean | undefined;
  /**
   * How multiple selection should behave in the collection.
   */
  selectionBehavior?: "replace" | "toggle" | undefined;
  /**
   * Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
   */
  by?: string | ((a: AcceptableValue, b: AcceptableValue) => boolean) | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

### Slots

```ts
/**
 * Slots for the CommandPalette component
 */
interface CommandPaletteSlots {
  empty(): any;
  footer(): any;
  back(): any;
  close(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the CommandPalette component
 */
interface CommandPaletteEmits {
  update:modelValue: (payload: [value: T]) => void;
  highlight: (payload: [payload: { ref: HTMLElement; value: T; } | undefined]) => void;
  entryFocus: (payload: [event: CustomEvent<any>]) => void;
  leave: (payload: [event: Event]) => void;
  update:open: (payload: [value: boolean]) => void;
  update:searchTerm: (payload: [value: string]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    commandPalette: {
      slots: {
        root: 'flex flex-col min-h-0 min-w-0 divide-y divide-default',
        input: '',
        close: '',
        back: 'p-0',
        content: 'relative overflow-hidden flex flex-col',
        footer: 'p-1',
        viewport: 'relative scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
        group: 'p-1 isolate',
        empty: 'text-center text-muted',
        label: 'font-semibold text-highlighted',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemLeadingChip: 'shrink-0',
        itemLeadingChipSize: '',
        itemTrailing: 'ms-auto inline-flex items-center',
        itemTrailingIcon: 'shrink-0',
        itemTrailingHighlightedIcon: 'shrink-0 text-dimmed hidden group-data-highlighted:inline-flex',
        itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
        itemTrailingKbdsSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate space-x-1 text-dimmed',
        itemDescription: 'truncate text-muted',
        itemLabelBase: 'text-highlighted [&>mark]:text-inverted [&>mark]:bg-primary',
        itemLabelPrefix: 'text-default',
        itemLabelSuffix: 'text-dimmed [&>mark]:text-inverted [&>mark]:bg-primary'
      },
      variants: {
        virtualize: {
          true: {
            viewport: 'p-1 isolate'
          },
          false: {
            viewport: 'divide-y divide-default'
          }
        },
        size: {
          xs: {
            input: '[&>input]:h-10',
            empty: 'py-3 text-xs',
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailing: 'gap-1',
            itemTrailingIcon: 'size-4',
            itemTrailingHighlightedIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          sm: {
            input: '[&>input]:h-11',
            empty: 'py-4 text-xs',
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailing: 'gap-1.5',
            itemTrailingIcon: 'size-4',
            itemTrailingHighlightedIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          md: {
            input: '[&>input]:h-12',
            empty: 'py-6 text-sm',
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailing: 'gap-1.5',
            itemTrailingIcon: 'size-5',
            itemTrailingHighlightedIcon: 'size-5',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'md'
          },
          lg: {
            input: '[&>input]:h-13',
            empty: 'py-7 text-sm',
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailing: 'gap-2',
            itemTrailingIcon: 'size-5',
            itemTrailingHighlightedIcon: 'size-5',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'md'
          },
          xl: {
            input: '[&>input]:h-14',
            empty: 'py-8 text-base',
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemLeadingChip: 'size-6',
            itemLeadingChipSize: 'lg',
            itemTrailing: 'gap-2',
            itemTrailingIcon: 'size-6',
            itemTrailingHighlightedIcon: 'size-6',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'lg'
          }
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
              'transition-colors'
            ]
          }
        },
        loading: {
          true: {
            itemLeadingIcon: 'animate-spin'
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/CommandPalette.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/command-palette.ts).


# Container

## Usage

Use the default slot to center and constrain the width of your content.

\> \[!TIP]
\> See: /docs/getting-started/theme/css-variables#container
\> Its max width is controlled by the \`--ui-container\` CSS variable.

```vue [ContainerExample.vue]
<template>
  <UContainer>
    <Placeholder class="h-32" />
  </UContainer>
</template>
```

## API

### Props

```ts
/**
 * Props for the Container component
 */
interface ContainerProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Container component
 */
interface ContainerSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    container: {
      base: 'w-full max-w-(--ui-container) mx-auto px-4 sm:px-6 lg:px-8'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Container.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/container.ts).


# ContentNavigation

\> \[!WARNING]
\> See: /docs/getting-started/integrations/content
\> This component is only available when the \`@nuxt/content\` module is installed.

## Usage

Use the `navigation` prop with the `navigation`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} value you get when fetching the navigation of your app.

```vue [ContentNavigationExample.vue]
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'

const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script>

<template>
  <UContentNavigation :navigation="navigation" highlight />
</template>
```

### Type

Set the `type` prop to `single` to only allow one item to be open at a time. Defaults to `multiple`.

```vue
<script setup lang="ts">
import type { ContentNavigationLink } from '@nuxt/ui'
</script>

<template>
  <UContentNavigation type="single" />
</template>
```

### Color

Use the `color` prop to change the color of the navigation links.

```vue
<script setup lang="ts">
import type { ContentNavigationLink } from '@nuxt/ui'
</script>

<template>
  <UContentNavigation color="neutral" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the navigation links.

```vue
<script setup lang="ts">
import type { ContentNavigationLink } from '@nuxt/ui'
</script>

<template>
  <UContentNavigation variant="link" />
</template>
```

### Highlight

Use the `highlight` prop to display a highlighted border for the active link.

Use the `highlight-color` prop to change the color of the border. It defaults to the `color` prop.

```vue
<script setup lang="ts">
import type { ContentNavigationLink } from '@nuxt/ui'
</script>

<template>
  <UContentNavigation highlight highlight-color="primary" color="primary" variant="pill" />
</template>
```

### Trailing Icon

```vue
<script setup lang="ts">
import type { ContentNavigationLink } from '@nuxt/ui'
</script>

<template>
  <UContentNavigation trailing-icon="i-lucide-arrow-up" />
</template>
```

## Examples

### Within a layout

Use the ContentNavigation component inside a [PageAside](https://ui.nuxt.com/docs/components/page-aside) component within a layout to display the navigation of the page:

```vue [layouts/docs.vue] {11}
<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" highlight />
      </UPageAside>
    </template>

    <slot />
  </UPage>
</template>
```

### Within a header

Use the ContentNavigation component inside the `content` slot of a [Header](https://ui.nuxt.com/docs/components/header) component to display the navigation of the page on mobile:

```vue [components/Header.vue] {9-11}
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'

const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script>

<template>
  <UHeader>
    <template #body>
      <UContentNavigation :navigation="navigation" highlight />
    </template>
  </UHeader>
</template>
```

## API

### Props

```ts
/**
 * Props for the ContentNavigation component
 */
interface ContentNavigationProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  /**
   * When `true`, the tree will be opened based on the current route.
   * When `false`, the tree will be closed.
   * When `undefined` (default), the first item will be opened with `type="single"` and the first level will be opened with `type="multiple"`.
   * @default "undefined"
   */
  defaultOpen?: boolean | undefined;
  /**
   * The icon displayed to toggle the accordion.
   */
  trailingIcon?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "pill" | "link" | undefined;
  /**
   * Display a line next to the active link.
   * @default "false"
   */
  highlight?: boolean | undefined;
  highlightColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * When type is "single", prevents closing the open item when clicking its trigger.
   * When type is "multiple", disables the collapsible behavior.
   * @default "true"
   */
  collapsible?: boolean | undefined;
  /**
   * @default "0"
   */
  level?: number | undefined;
  navigation?: T[] | undefined;
  ui?: { root?: ClassNameValue; content?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; listWithChildren?: ClassNameValue; itemWithChildren?: ClassNameValue; trigger?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkTrailing?: ClassNameValue; linkTrailingBadge?: ClassNameValue; linkTrailingBadgeSize?: ClassNameValue; linkTrailingIcon?: ClassNameValue; linkTitle?: ClassNameValue; linkTitleExternalIcon?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the accordion and all its items
   */
  disabled?: boolean | undefined;
  /**
   * Determines whether a "single" or "multiple" items can be selected at a time.
   * 
   * This prop will overwrite the inferred type from `modelValue` and `defaultValue`.
   * @default "\"multiple\""
   */
  type?: SingleOrMultipleType | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "true"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ContentNavigation component
 */
interface ContentNavigationSlots {
  link(): any;
  link-leading(): any;
  link-title(): any;
  link-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ContentNavigation component
 */
interface ContentNavigationEmits {
  update:modelValue: (payload: [value: string | string[] | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contentNavigation: {
      slots: {
        root: '',
        content: 'data-[state=open]:animate-[accordion-down_200ms_ease-out] data-[state=closed]:animate-[accordion-up_200ms_ease-out] overflow-hidden focus:outline-none',
        list: 'isolate -mx-2.5 -mt-1.5',
        item: '',
        listWithChildren: 'ms-5 border-s border-default',
        itemWithChildren: 'flex flex-col data-[state=open]:mb-1.5',
        trigger: 'font-semibold',
        link: 'group relative w-full px-2.5 py-1.5 before:inset-y-px before:inset-x-0 flex items-center gap-1.5 text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
        linkLeadingIcon: 'shrink-0 size-5',
        linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        linkTrailingBadge: 'shrink-0',
        linkTrailingBadgeSize: 'sm',
        linkTrailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180',
        linkTitle: 'truncate',
        linkTitleExternalIcon: 'size-3 align-top text-dimmed'
      },
      variants: {
        color: {
          primary: {
            trigger: 'focus-visible:ring-primary',
            link: 'focus-visible:before:ring-primary'
          },
          secondary: {
            trigger: 'focus-visible:ring-secondary',
            link: 'focus-visible:before:ring-secondary'
          },
          success: {
            trigger: 'focus-visible:ring-success',
            link: 'focus-visible:before:ring-success'
          },
          info: {
            trigger: 'focus-visible:ring-info',
            link: 'focus-visible:before:ring-info'
          },
          warning: {
            trigger: 'focus-visible:ring-warning',
            link: 'focus-visible:before:ring-warning'
          },
          error: {
            trigger: 'focus-visible:ring-error',
            link: 'focus-visible:before:ring-error'
          },
          neutral: {
            trigger: 'focus-visible:ring-inverted',
            link: 'focus-visible:before:ring-inverted'
          }
        },
        highlightColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          pill: '',
          link: ''
        },
        active: {
          true: {
            link: 'font-medium'
          },
          false: {
            link: 'text-muted',
            linkLeadingIcon: 'text-dimmed'
          }
        },
        disabled: {
          true: {
            trigger: 'data-[state=open]:text-highlighted'
          }
        },
        highlight: {
          true: {}
        },
        level: {
          true: {
            item: 'ps-1.5 -ms-px',
            itemWithChildren: 'ps-1.5 -ms-px'
          }
        }
      },
      compoundVariants: [
        {
          highlight: true,
          level: true,
          class: {
            link: [
              'after:absolute after:-left-1.5 after:inset-y-0.5 after:block after:w-px after:rounded-full',
              'after:transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'pill',
          class: {
            link: [
              'hover:text-highlighted hover:before:bg-elevated/50 data-[state=open]:text-highlighted',
              'transition-colors before:transition-colors'
            ],
            linkLeadingIcon: [
              'group-hover:text-default group-data-[state=open]:text-default',
              'transition-colors'
            ]
          }
        },
        {
          color: 'primary',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-primary',
            linkLeadingIcon: 'text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-secondary',
            linkLeadingIcon: 'text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-success',
            linkLeadingIcon: 'text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-info',
            linkLeadingIcon: 'text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-warning',
            linkLeadingIcon: 'text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-error',
            linkLeadingIcon: 'text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-highlighted',
            linkLeadingIcon: 'text-highlighted group-data-[state=open]:text-highlighted'
          }
        },
        {
          variant: 'pill',
          active: true,
          highlight: false,
          class: {
            link: 'before:bg-elevated'
          }
        },
        {
          variant: 'pill',
          active: true,
          highlight: true,
          disabled: false,
          class: {
            link: [
              'hover:before:bg-elevated/50',
              'before:transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'link',
          class: {
            link: [
              'hover:text-highlighted data-[state=open]:text-highlighted',
              'transition-colors'
            ],
            linkLeadingIcon: [
              'group-hover:text-default group-data-[state=open]:text-default',
              'transition-colors'
            ]
          }
        },
        {
          color: 'primary',
          variant: 'link',
          active: true,
          class: {
            link: 'text-primary',
            linkLeadingIcon: 'text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'link',
          active: true,
          class: {
            link: 'text-secondary',
            linkLeadingIcon: 'text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          variant: 'link',
          active: true,
          class: {
            link: 'text-success',
            linkLeadingIcon: 'text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          variant: 'link',
          active: true,
          class: {
            link: 'text-info',
            linkLeadingIcon: 'text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          variant: 'link',
          active: true,
          class: {
            link: 'text-warning',
            linkLeadingIcon: 'text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          variant: 'link',
          active: true,
          class: {
            link: 'text-error',
            linkLeadingIcon: 'text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          active: true,
          class: {
            link: 'text-highlighted',
            linkLeadingIcon: 'text-highlighted group-data-[state=open]:text-highlighted'
          }
        },
        {
          highlightColor: 'primary',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-primary'
          }
        },
        {
          highlightColor: 'secondary',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-secondary'
          }
        },
        {
          highlightColor: 'success',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-success'
          }
        },
        {
          highlightColor: 'info',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-info'
          }
        },
        {
          highlightColor: 'warning',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-warning'
          }
        },
        {
          highlightColor: 'error',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-error'
          }
        },
        {
          highlightColor: 'neutral',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-inverted'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        highlightColor: 'primary',
        variant: 'pill'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/content/ContentNavigation.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/content/content-navigation.ts).


# ContentSearch

\> \[!WARNING]
\> See: /docs/getting-started/integrations/content
\> This component is only available when the \`@nuxt/content\` module is installed.

## Usage

The ContentSearch component extends the [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) component, so you can pass any property such as `icon`, `placeholder`, etc.

Use the `files` and `navigation` props with the `files`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} and `navigation`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} values you fetched using the `queryCollectionSearchSections` and `queryCollectionNavigation` composables from `@nuxt/content`.

```vue [ContentSearchExample.vue]
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'

const { data: files } = useLazyAsyncData('content-search-example', () => queryCollectionSearchSections('docs'), {
  server: false
})

const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')

const searchTerm = ref('')
</script>

<template>
  <ClientOnly>
    <LazyUContentSearch
      v-model:search-term="searchTerm"
      open
      :autofocus="false"
      :files="files"
      :navigation="navigation"
      :fuse="{ resultLimit: 42 }"
    />
  </ClientOnly>
</template>
```

\> \[!TIP]
\> You can open the CommandPalette by pressing , by using the \[ContentSearchButton]\(/docs/components/content-search-button) component or by using the \`useContentSearch\` composable: \`const { open } = useContentSearch()\`.

### Shortcut

Use the `shortcut` prop to change the shortcut used in [defineShortcuts](https://ui.nuxt.com/docs/composables/define-shortcuts) to open the ContentSearch component. Defaults to `meta_k` ( :kbd{value="meta"} :kbd{value="K"} ).

```vue [app.vue] {6}
<template>
  <UApp>
    <ClientOnly>
      <LazyUContentSearch
        v-model:search-term="searchTerm"
        shortcut="meta_k"
        :files="files"
        :navigation="navigation"
        :fuse="{ resultLimit: 42 }"
      />
    </ClientOnly>
  </UApp>
</template>
```

### Color Mode

By default, a group of commands will be added to the command palette so you can switch between light and dark mode. This will only take effect if the `colorMode` is not forced in a specific page which can be achieved through `definePageMeta`:

```vue [pages/index.vue]
<script setup lang="ts">
definePageMeta({
  colorMode: 'dark'
})
</script>
```

You can disable this behavior by setting the `color-mode` prop to `false`:

```vue [app.vue] {6}
<template>
  <UApp>
    <ClientOnly>
      <LazyUContentSearch
        v-model:search-term="searchTerm"
        :color-mode="false"
        :files="files"
        :navigation="navigation"
        :fuse="{ resultLimit: 42 }"
      />
    </ClientOnly>
  </UApp>
</template>
```

## Examples

### Within `app.vue`

Use the ContentSearch component in your `app.vue` or in a layout:

```vue [app.vue]
<script setup lang="ts">
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
  server: false
})

const links = [{
  label: 'Docs',
  icon: 'i-lucide-book',
  to: '/docs/getting-started'
}, {
  label: 'Components',
  icon: 'i-lucide-box',
  to: '/docs/components'
}, {
  label: 'Showcase',
  icon: 'i-lucide-presentation',
  to: '/showcase'
}]

const searchTerm = ref('')
</script>

<template>
  <UApp>
    <ClientOnly>
      <LazyUContentSearch
        v-model:search-term="searchTerm"
        :files="files"
        shortcut="meta_k"
        :navigation="navigation"
        :links="links"
        :fuse="{ resultLimit: 42 }"
      />
    </ClientOnly>
  </UApp>
</template>
```

\> \[!TIP]
\> It is recommended to wrap the \`ContentSearch\` component in a \[ClientOnly]\(https\://nuxt.com/docs/api/components/client-only) component so it's not rendered on the server.

## API

### Props

```ts
/**
 * Props for the ContentSearch component
 */
interface ContentSearchProps {
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  /**
   * The icon displayed in the input.
   */
  icon?: any;
  /**
   * The placeholder text for the input.
   */
  placeholder?: string | undefined;
  /**
   * Automatically focus the input when component is mounted.
   */
  autofocus?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * Display a close button in the input (useful when inside a Modal for example).
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * Keyboard shortcut to open the search (used by [`defineShortcuts`](https://ui.nuxt.com/docs/composables/define-shortcuts))
   * @default "\"meta_k\""
   */
  shortcut?: string | undefined;
  /**
   * Links group displayed as the first group in the command palette.
   */
  links?: T[] | undefined;
  navigation?: ContentNavigationItem[] | undefined;
  /**
   * Custom groups displayed between navigation and color mode group.
   */
  groups?: CommandPaletteGroup<ContentSearchItem>[] | undefined;
  files?: ContentSearchFile[] | undefined;
  /**
   * Options for [useFuse](https://vueuse.org/integrations/useFuse) passed to the [CommandPalette](https://ui.nuxt.com/docs/components/command-palette).
   */
  fuse?: n<T> | undefined;
  /**
   * When `true`, the theme command will be added to the groups.
   * @default "true"
   */
  colorMode?: boolean | undefined;
  ui?: ({ modal?: ClassNameValue; input?: ClassNameValue; } & { root?: ClassNameValue; input?: ClassNameValue; close?: ClassNameValue; back?: ClassNameValue; content?: ClassNameValue; footer?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingHighlightedIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelBase?: ClassNameValue; itemLabelPrefix?: ClassNameValue; itemLabelSuffix?: ClassNameValue; }) | undefined;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Render an overlay behind the modal.
   */
  overlay?: boolean | undefined;
  /**
   * Animate the modal when opening or closing.
   */
  transition?: boolean | undefined;
  /**
   * The content of the modal.
   */
  content?: (Omit<DialogContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DialogContentImplEmits>>) | undefined;
  /**
   * When `false`, the modal will not close when clicking outside or pressing escape.
   */
  dismissible?: boolean | undefined;
  /**
   * When `true`, the modal will take up the full screen.
   * @default "false"
   */
  fullscreen?: boolean | undefined;
  /**
   * The modality of the dialog When set to `true`, <br>
   * interaction with outside elements will be disabled and only dialog content will be visible to screen readers.
   */
  modal?: boolean | undefined;
  /**
   * Render the modal in a portal.
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ContentSearch component
 */
interface ContentSearchSlots {
  empty(): any;
  footer(): any;
  back(): any;
  close(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ContentSearch component
 */
interface ContentSearchEmits {
  update:searchTerm: (payload: [value: string]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                                    | Type                                                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commandPaletteRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<InstanceType<typeof UCommandPalette> | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contentSearch: {
      slots: {
        modal: '',
        input: ''
      },
      variants: {
        fullscreen: {
          false: {
            modal: 'sm:max-w-3xl h-full sm:h-[28rem]'
          }
        },
        size: {
          xs: {},
          sm: {},
          md: {},
          lg: {},
          xl: {}
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/content/ContentSearch.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/content/content-search.ts).


# ContentSearchButton

\> \[!WARNING]
\> See: /docs/getting-started/integrations/content
\> This component is only available when the \`@nuxt/content\` module is installed.

## Usage

The ContentSearchButton component is used to open the [ContentSearch](https://ui.nuxt.com/docs/components/content-search) modal.

```vue
<template>
  <UContentSearchButton />
</template>
```

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UContentSearchButton variant="subtle" />
</template>
```

\> \[!NOTE]
\> See: #collapsed
\> The button defaults to \`color="neutral"\` and \`variant="outline"\` when not collapsed, \`variant="ghost"\` when collapsed.

### Collapsed

Use the `collapsed` prop to show the button's label and [kbds](https://ui.nuxt.com/#kbds). Defaults to `true`.

```vue
<template>
  <UContentSearchButton :collapsed="false" />
</template>
```

### Kbds

Use the `kbds` prop to display keyboard keys in the button. Defaults to `['meta', 'K']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} to match the default shortcut of the [ContentSearch](https://ui.nuxt.com/docs/components/content-search#shortcut) component.

```vue
<template>
  <UContentSearchButton :collapsed="false" />
</template>
```

## API

### Props

```ts
/**
 * Props for the ContentSearchButton component
 */
interface ContentSearchButtonProps {
  /**
   * The icon displayed in the button.
   */
  icon?: any;
  /**
   * The label displayed in the button.
   */
  label?: string | undefined;
  /**
   * The color of the button.
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * The variant of the button.
   * Defaults to 'outline' when not collapsed, 'ghost' when collapsed.
   */
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * Whether the button is collapsed.
   * @default "true"
   */
  collapsed?: boolean | undefined;
  /**
   * Display a tooltip on the button when is collapsed with the button label.
   * This has priority over the global `tooltip` prop.
   * @default "false"
   */
  tooltip?: boolean | TooltipProps | undefined;
  /**
   * The keyboard keys to display in the button.
   * `{ variant: 'subtle' }`{lang="ts-type"}
   * @default "[\"meta\", \"k\"]"
   */
  kbds?: (string | undefined)[] | KbdProps[] | undefined;
  ui?: ({ base?: ClassNameValue; label?: ClassNameValue; trailing?: ClassNameValue; } & { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; }) | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  activeVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the ContentSearchButton component
 */
interface ContentSearchButtonSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contentSearchButton: {
      slots: {
        base: '',
        label: '',
        trailing: 'hidden lg:flex items-center gap-0.5 ms-auto'
      },
      variants: {
        collapsed: {
          true: {
            label: 'hidden',
            trailing: 'lg:hidden'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/content/ContentSearchButton.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/content/content-search-button.ts).


# ContentSurround

\> \[!WARNING]
\> See: /docs/getting-started/integrations/content
\> This component is only available when the \`@nuxt/content\` module is installed.

## Usage

Use the `surround` prop with the `surround`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} value you get when fetching a page surround.

```vue [ContentSurroundExample.vue]
<script setup lang="ts">
const route = useRoute()

const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
  return queryCollectionItemSurroundings('docs', route.path, {
    fields: ['description']
  })
})
</script>

<template>
  <UContentSurround :surround="(surround as any)" />
</template>
```

### Prev / Next

Use the `prev-icon` and `next-icon` props to customize the buttons [Icon](https://ui.nuxt.com/docs/components/icon).

```vue
<script setup lang="ts">
import type { ContentSurroundLink } from '@nuxt/ui'
</script>

<template>
  <UContentSurround prev-icon="i-lucide-chevron-left" next-icon="i-lucide-chevron-right" />
</template>
```

## Examples

### Within a page

Use the ContentSurround component in a page to display the prev and next links:

```vue [pages/[...slug\\].vue] {19}
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UPage v-if="page">
    <UPageHeader :title="page.title" />

    <UPageBody>
      <ContentRenderer v-if="page.body" :value="page" />

      <USeparator v-if="surround?.filter(Boolean).length" />

      <UContentSurround :surround="(surround as any)" />
    </UPageBody>

    <template v-if="page?.body?.toc?.links?.length" #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>
```

## API

### Props

```ts
/**
 * Props for the ContentSurround component
 */
interface ContentSurroundProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed in the prev link.
   */
  prevIcon?: any;
  /**
   * The icon displayed in the next link.
   */
  nextIcon?: any;
  surround?: T[] | undefined;
  ui?: { root?: ClassNameValue; link?: ClassNameValue; linkLeading?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkTitle?: ClassNameValue; linkDescription?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ContentSurround component
 */
interface ContentSurroundSlots {
  link(): any;
  link-leading(): any;
  link-title(): any;
  link-description(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contentSurround: {
      slots: {
        root: 'grid grid-cols-1 sm:grid-cols-2 gap-8',
        link: [
          'group block px-6 py-8 rounded-lg border border-default hover:bg-elevated/50 focus-visible:outline-primary',
          'transition-colors'
        ],
        linkLeading: [
          'inline-flex items-center rounded-full p-1.5 bg-elevated group-hover:bg-primary/10 ring ring-accented mb-4 group-hover:ring-primary/50',
          'transition'
        ],
        linkLeadingIcon: [
          'size-5 shrink-0 text-highlighted group-hover:text-primary',
          'transition-[color,translate]'
        ],
        linkTitle: 'font-medium text-[15px] text-highlighted mb-1 truncate',
        linkDescription: 'text-sm text-muted line-clamp-2'
      },
      variants: {
        direction: {
          left: {
            linkLeadingIcon: [
              'group-active:-translate-x-0.5'
            ]
          },
          right: {
            link: 'text-end',
            linkLeadingIcon: [
              'group-active:translate-x-0.5'
            ]
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/content/ContentSurround.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/content/content-surround.ts).


# ContentToc

\> \[!WARNING]
\> See: /docs/getting-started/integrations/content
\> This component is only available when the \`@nuxt/content\` module is installed.

## Usage

Use the `links` prop with the `page?.body?.toc?.links`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} you get when fetching a page.

```vue [ContentTocExample.vue]
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UContentToc :links="page?.body?.toc?.links" />
</template>
```

### Title

Use the `title` prop to change the title of the Table of Contents.

```vue
<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'
</script>

<template>
  <UContentToc title="On this page" />
</template>
```

### Color

Use the `color` prop to change the color of the links.

```vue
<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'
</script>

<template>
  <UContentToc color="neutral" />
</template>
```

### Highlight

Use the `highlight` prop to display a highlighted border for the active item.

```vue
<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'
</script>

<template>
  <UContentToc highlight />
</template>
```

### Highlight Color

Use the `highlight-color` prop to change the color of the highlight. It defaults to the `color` prop.

```vue
<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'
</script>

<template>
  <UContentToc highlight highlight-color="neutral" />
</template>
```

### Highlight Variant `Soon`

Use the `highlight-variant` prop to change the style of the highlight. Defaults to `straight`.

```vue
<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'
</script>

<template>
  <UContentToc highlight highlight-color="primary" highlight-variant="circuit" />
</template>
```

## Examples

### Within a page

Use the ContentToc component in a page to display the Table of Contents:

```vue [pages/[...slug\\].vue] {22-24}
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UPage v-if="page">
    <UPageHeader :title="page.title" />

    <UPageBody>
      <ContentRenderer v-if="page.body" :value="page" />

      <USeparator v-if="surround?.filter(Boolean).length" />

      <UContentSurround :surround="(surround as any)" />
    </UPageBody>

    <template v-if="page?.body?.toc?.links?.length" #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>
```

## API

### Props

```ts
/**
 * Props for the ContentToc component
 */
interface ContentTocProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  /**
   * The icon displayed to collapse the content.
   */
  trailingIcon?: any;
  /**
   * The title of the table of contents.
   */
  title?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * Display a line next to the active link.
   */
  highlight?: boolean | undefined;
  highlightColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The variant of the highlight indicator.
   */
  highlightVariant?: "straight" | "circuit" | undefined;
  links?: T[] | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; top?: ClassNameValue; bottom?: ClassNameValue; trigger?: ClassNameValue; title?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; list?: ClassNameValue; listWithChildren?: ClassNameValue; item?: ClassNameValue; itemWithChildren?: ClassNameValue; link?: ClassNameValue; linkText?: ClassNameValue; indicator?: ClassNameValue; indicatorLine?: ClassNameValue; indicatorActive?: ClassNameValue; } | undefined;
  /**
   * The open state of the collapsible when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the collapsible. Can be binded with `v-model`.
   */
  open?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ContentToc component
 */
interface ContentTocSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  content(): any;
  link(): any;
  top(): any;
  bottom(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ContentToc component
 */
interface ContentTocEmits {
  update:open: (payload: [value: boolean]) => void;
  move: (payload: [id: string]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contentToc: {
      slots: {
        root: 'sticky top-(--ui-header-height) z-10 bg-default/75 lg:bg-[initial] backdrop-blur -mx-4 px-4 sm:px-6 sm:-mx-6 lg:ms-0 overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
        container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-default lg:border-0 flex flex-col',
        top: '',
        bottom: 'hidden lg:flex lg:flex-col gap-6',
        trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 focus-visible:outline-primary',
        title: 'truncate',
        trailing: 'ms-auto inline-flex gap-1.5 items-center',
        trailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
        content: 'relative data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden focus:outline-none',
        list: 'min-w-0',
        listWithChildren: 'ms-3',
        item: 'min-w-0',
        itemWithChildren: '',
        link: 'group relative text-sm flex items-center focus-visible:outline-primary py-1',
        linkText: 'truncate',
        indicator: '',
        indicatorLine: '',
        indicatorActive: ''
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        highlightColor: {
          primary: {
            indicatorActive: 'bg-primary'
          },
          secondary: {
            indicatorActive: 'bg-secondary'
          },
          success: {
            indicatorActive: 'bg-success'
          },
          info: {
            indicatorActive: 'bg-info'
          },
          warning: {
            indicatorActive: 'bg-warning'
          },
          error: {
            indicatorActive: 'bg-error'
          },
          neutral: {
            indicatorActive: 'bg-inverted'
          }
        },
        active: {
          false: {
            link: [
              'text-muted hover:text-default',
              'transition-colors'
            ]
          }
        },
        highlight: {
          true: ''
        },
        highlightVariant: {
          straight: '',
          circuit: ''
        },
        body: {
          true: {
            bottom: 'mt-6'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          active: true,
          class: {
            link: 'text-primary'
          }
        },
        {
          color: 'secondary',
          active: true,
          class: {
            link: 'text-secondary'
          }
        },
        {
          color: 'success',
          active: true,
          class: {
            link: 'text-success'
          }
        },
        {
          color: 'info',
          active: true,
          class: {
            link: 'text-info'
          }
        },
        {
          color: 'warning',
          active: true,
          class: {
            link: 'text-warning'
          }
        },
        {
          color: 'error',
          active: true,
          class: {
            link: 'text-error'
          }
        },
        {
          color: 'neutral',
          active: true,
          class: {
            link: 'text-highlighted'
          }
        },
        {
          highlight: true,
          highlightVariant: 'straight',
          class: {
            list: 'ms-2.5 ps-4 border-s border-default',
            item: '-ms-px',
            indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full',
            indicatorLine: 'hidden',
            indicatorActive: 'w-full h-full'
          }
        },
        {
          highlight: true,
          highlightVariant: 'circuit',
          class: {
            list: 'ps-6.5',
            item: '-ms-px',
            itemWithChildren: 'ps-px',
            indicator: 'absolute ms-2.5 start-0 top-0 rtl:-scale-x-100',
            indicatorLine: 'absolute inset-0 bg-(--ui-border)',
            indicatorActive: 'absolute w-full h-(--indicator-size) translate-y-(--indicator-position) transition-[translate,height] duration-200 ease-out'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        highlightColor: 'primary',
        highlightVariant: 'straight'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/content/ContentToc.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/content/content-toc.ts).


# ContextMenu

## Usage

Use anything you like in the default slot of the ContextMenu, and right-click on it to display the menu.

```vue
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor',
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun',
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon',
        },
      ],
    },
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: [
        'meta',
        's',
      ],
    },
    {
      label: 'Show Toolbar',
      kbds: [
        'shift',
        'meta',
        'd',
      ],
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true,
    },
  ],
  [
    {
      label: 'Refresh the Page',
    },
    {
      label: 'Clear Cookies and Refresh',
    },
    {
      label: 'Clear Cache and Refresh',
    },
    {
      type: 'separator',
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: [
              'meta',
              'shift',
              'u',
            ],
          },
          {
            label: 'Developer Tools',
            kbds: [
              'option',
              'meta',
              'i',
            ],
          },
          {
            label: 'Inspect Elements',
            kbds: [
              'option',
              'meta',
              'c',
            ],
          },
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: [
              'option',
              'meta',
              'j',
            ],
          },
        ],
      ],
    },
  ],
])
</script>

<template>
  <UContextMenu :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `kbds?: string[] | KbdProps[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`type?: "link" | "label" | "separator" | "checkbox"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-color-items)
- [`checked?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`onUpdateChecked?: (checked: boolean) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- `children?: ContextMenuItem[] | ContextMenuItem[][]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor',
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun',
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon',
        },
      ],
    },
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: [
        'meta',
        's',
      ],
    },
    {
      label: 'Show Toolbar',
      kbds: [
        'shift',
        'meta',
        'd',
      ],
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true,
    },
  ],
  [
    {
      label: 'Refresh the Page',
    },
    {
      label: 'Clear Cookies and Refresh',
    },
    {
      label: 'Clear Cache and Refresh',
    },
    {
      type: 'separator',
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: [
              'meta',
              'shift',
              'u',
            ],
          },
          {
            label: 'Developer Tools',
            kbds: [
              'option',
              'meta',
              'i',
            ],
          },
          {
            label: 'Inspect Elements',
            kbds: [
              'option',
              'meta',
              'c',
            ],
          },
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: [
              'option',
              'meta',
              'j',
            ],
          },
        ],
      ],
    },
  ],
])
</script>

<template>
  <UContextMenu :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

\> \[!TIP]
\> Each item can take a \`children\` array of objects with the same properties as the \`items\` prop to create a nested menu which can be controlled using the \`open\`, \`defaultOpen\` and \`content\` properties.

### Size

Use the `size` prop to change the size of the ContextMenu.

```vue
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor',
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun',
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon',
  },
])
</script>

<template>
  <UContextMenu size="xl" :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

### Modal

Use the `modal` prop to control whether the ContextMenu blocks interaction with outside content. Defaults to `true`.

```vue
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor',
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun',
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon',
  },
])
</script>

<template>
  <UContextMenu :modal="false" :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

### Disabled

Use the `disabled` prop to disable the ContextMenu.

```vue
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor',
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun',
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon',
  },
])
</script>

<template>
  <UContextMenu disabled :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

## Examples

### With checkbox items

You can use the `type` property with `checkbox` and use the `checked` / `onUpdateChecked` properties to control the checked state of the item.

```vue [ContextMenuCheckboxItemsExample.vue]
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const showSidebar = ref(true)
const showToolbar = ref(false)

const items = computed<ContextMenuItem[]>(() => [{
  label: 'View',
  type: 'label' as const
}, {
  type: 'separator' as const
}, {
  label: 'Show Sidebar',
  type: 'checkbox' as const,
  checked: showSidebar.value,
  onUpdateChecked(checked: boolean) {
    showSidebar.value = checked
  },
  onSelect(e: Event) {
    e.preventDefault()
  }
}, {
  label: 'Show Toolbar',
  type: 'checkbox' as const,
  checked: showToolbar.value,
  onUpdateChecked(checked: boolean) {
    showToolbar.value = checked
  }
}, {
  label: 'Collapse Pinned Tabs',
  type: 'checkbox' as const,
  disabled: true
}])
</script>

<template>
  <UContextMenu :items="items" :ui="{ content: 'w-48' }">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

\> \[!NOTE]
\> To ensure reactivity for the \`checked\` state of items, it's recommended to wrap your \`items\` array inside a \`computed\`.

### With color items

You can use the `color` property to highlight certain items with a color.

```vue [ContextMenuColorItemsExample.vue]
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const items: ContextMenuItem[][] = [
  [
    {
      label: 'View',
      icon: 'i-lucide-eye'
    },
    {
      label: 'Copy',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Edit',
      icon: 'i-lucide-pencil'
    }
  ],
  [
    {
      label: 'Delete',
      color: 'error' as const,
      icon: 'i-lucide-trash'
    }
  ]
]
</script>

<template>
  <UContextMenu :items="items" :ui="{ content: 'w-48' }">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>
  </UContextMenu>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [ContextMenuCustomSlotExample.vue]
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'

const loading = ref(true)

const items = [
  {
    label: 'Refresh the Page',
    slot: 'refresh' as const
  },
  {
    label: 'Clear Cookies and Refresh'
  },
  {
    label: 'Clear Cache and Refresh'
  }
] satisfies ContextMenuItem[]
</script>

<template>
  <UContextMenu :items="items" :ui="{ content: 'w-48' }">
    <div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
      Right click here
    </div>

    <template #refresh-label>
      {{ loading ? 'Refreshing...' : 'Refresh the Page' }}
    </template>

    <template #refresh-trailing>
      <UIcon v-if="loading" name="i-lucide-loader-circle" class="shrink-0 size-5 text-primary animate-spin" />
    </template>
  </UContextMenu>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can also use the \`#item\`, \`#item-leading\`, \`#item-label\` and \`#item-trailing\` slots to customize all items.

### Extract shortcuts

Use the [extractShortcuts](https://ui.nuxt.com/docs/composables/extract-shortcuts) utility to automatically define shortcuts from menu items with a `kbds` property. It recursively extracts shortcuts and returns an object compatible with [defineShortcuts](https://ui.nuxt.com/docs/composables/define-shortcuts).

```vue
<script setup lang="ts">
const items = [
  [{
    label: 'Show Sidebar',
    kbds: ['meta', 'S'],
    onSelect() {
      console.log('Show Sidebar clicked')
    }
  }, {
    label: 'Show Toolbar',
    kbds: ['shift', 'meta', 'D'],
    onSelect() {
      console.log('Show Toolbar clicked')
    }
  }, {
    label: 'Collapse Pinned Tabs',
    disabled: true
  }], [{
    label: 'Refresh the Page'
  }, {
    label: 'Clear Cookies and Refresh'
  }, {
    label: 'Clear Cache and Refresh'
  }, {
    type: 'separator' as const
  }, {
    label: 'Developer',
    children: [[{
      label: 'View Source',
      kbds: ['option', 'meta', 'U'],
      onSelect() {
        console.log('View Source clicked')
      }
    }, {
      label: 'Developer Tools',
      kbds: ['option', 'meta', 'I'],
      onSelect() {
        console.log('Developer Tools clicked')
      }
    }], [{
      label: 'Inspect Elements',
      kbds: ['option', 'meta', 'C'],
      onSelect() {
        console.log('Inspect Elements clicked')
      }
    }], [{
      label: 'JavaScript Console',
      kbds: ['option', 'meta', 'J'],
      onSelect() {
        console.log('JavaScript Console clicked')
      }
    }]]
  }]
]

defineShortcuts(extractShortcuts(items))
</script>
```

\> \[!NOTE]
\> In this example, , , , , and would trigger the \`select\` function of the corresponding item.

## API

### Props

```ts
/**
 * Props for the ContextMenu component
 */
interface ContextMenuProps {
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  items?: T | undefined;
  /**
   * The icon displayed when an item is checked.
   */
  checkedIcon?: any;
  /**
   * The icon displayed when an item is loading.
   */
  loadingIcon?: any;
  /**
   * The icon displayed when the item is an external link.
   * Set to `false` to hide the external icon.
   * @default "true"
   */
  externalIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<ContextMenuContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<MenuContentEmits>>) | undefined;
  /**
   * Render the menu in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the description from the item.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  disabled?: boolean | undefined;
  ui?: { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } | undefined;
  /**
   * The modality of the dropdown menu.
   * 
   * When set to `true`, interaction with outside elements will be disabled and only menu content will be visible to screen readers.
   * @default "true"
   */
  modal?: boolean | undefined;
  /**
   * The duration from when the trigger is pressed until the menu opens.
   */
  pressOpenDelay?: number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ContextMenu component
 */
interface ContextMenuSlots {
  default(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  content-top(): any;
  content-bottom(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ContextMenu component
 */
interface ContextMenuEmits {
  update:open: (payload: [payload: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    contextMenu: {
      slots: {
        content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) flex flex-col',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0',
        itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
        itemTrailingKbdsSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:text-highlighted data-[state=open]:text-highlighted data-highlighted:before:bg-elevated/50 data-[state=open]:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:text-default group-data-[state=open]:text-default',
              'transition-colors'
            ]
          }
        },
        loading: {
          true: {
            itemLeadingIcon: 'animate-spin'
          }
        },
        size: {
          xs: {
            label: 'p-1 text-xs gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemTrailingIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          sm: {
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemTrailingIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          md: {
            label: 'p-1.5 text-sm gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemTrailingIcon: 'size-5',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'md'
          },
          lg: {
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemTrailingIcon: 'size-5',
            itemTrailingKbds: 'gap-1',
            itemTrailingKbdsSize: 'md'
          },
          xl: {
            label: 'p-2 text-base gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemTrailingIcon: 'size-6',
            itemTrailingKbds: 'gap-1',
            itemTrailingKbdsSize: 'lg'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          active: false,
          class: {
            item: 'text-primary data-highlighted:text-primary data-highlighted:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
            itemLeadingIcon: 'text-primary/75 group-data-highlighted:text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          active: false,
          class: {
            item: 'text-secondary data-highlighted:text-secondary data-highlighted:before:bg-secondary/10 data-[state=open]:before:bg-secondary/10',
            itemLeadingIcon: 'text-secondary/75 group-data-highlighted:text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          active: false,
          class: {
            item: 'text-success data-highlighted:text-success data-highlighted:before:bg-success/10 data-[state=open]:before:bg-success/10',
            itemLeadingIcon: 'text-success/75 group-data-highlighted:text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          active: false,
          class: {
            item: 'text-info data-highlighted:text-info data-highlighted:before:bg-info/10 data-[state=open]:before:bg-info/10',
            itemLeadingIcon: 'text-info/75 group-data-highlighted:text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          active: false,
          class: {
            item: 'text-warning data-highlighted:text-warning data-highlighted:before:bg-warning/10 data-[state=open]:before:bg-warning/10',
            itemLeadingIcon: 'text-warning/75 group-data-highlighted:text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          active: false,
          class: {
            item: 'text-error data-highlighted:text-error data-highlighted:before:bg-error/10 data-[state=open]:before:bg-error/10',
            itemLeadingIcon: 'text-error/75 group-data-highlighted:text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'primary',
          active: true,
          class: {
            item: 'text-primary before:bg-primary/10',
            itemLeadingIcon: 'text-primary'
          }
        },
        {
          color: 'secondary',
          active: true,
          class: {
            item: 'text-secondary before:bg-secondary/10',
            itemLeadingIcon: 'text-secondary'
          }
        },
        {
          color: 'success',
          active: true,
          class: {
            item: 'text-success before:bg-success/10',
            itemLeadingIcon: 'text-success'
          }
        },
        {
          color: 'info',
          active: true,
          class: {
            item: 'text-info before:bg-info/10',
            itemLeadingIcon: 'text-info'
          }
        },
        {
          color: 'warning',
          active: true,
          class: {
            item: 'text-warning before:bg-warning/10',
            itemLeadingIcon: 'text-warning'
          }
        },
        {
          color: 'error',
          active: true,
          class: {
            item: 'text-error before:bg-error/10',
            itemLeadingIcon: 'text-error'
          }
        }
      ],
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ContextMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/context-menu.ts).


# DashboardGroup

## Usage

The DashboardGroup component is the main layout that wraps the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) and [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) components to create a responsive dashboard interface.

Use it in a layout or in your `app.vue`:

```vue [layouts/dashboard.vue] {2,6}
<template>
  <UDashboardGroup>
    <UDashboardSidebar />

    <slot />
  </UDashboardGroup>
</template>
```

## API

### Props

```ts
/**
 * Props for the DashboardGroup component
 */
interface DashboardGroupProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
  /**
   * The storage to use for the size.
   * @default "\"cookie\""
   */
  storage?: "cookie" | "local" | undefined;
  /**
   * Unique id used to auto-save size.
   * @default "\"dashboard\""
   */
  storageKey?: string | undefined;
  /**
   * Whether to persist the size in the storage.
   * @default "true"
   */
  persistent?: boolean | undefined;
  /**
   * The unit to use for size values.
   * @default "\"%\""
   */
  unit?: "%" | "rem" | "px" | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardGroup component
 */
interface DashboardGroupSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardGroup: {
      base: 'fixed inset-0 flex overflow-hidden'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardGroup.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-group.ts).


# DashboardNavbar

## Usage

The DashboardNavbar component is a responsive navigation bar that integrates with the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) component. It includes a mobile toggle button to enable responsive navigation in dashboard layouts.

Use it inside the `header` slot of the [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) component:

```vue [pages/index.vue] {9-11}
<script setup lang="ts">
definePageMeta({
  layout: 'dashboard'
})
</script>

<template>
  <UDashboardPanel>
    <template #header>
      <UDashboardNavbar />
    </template>
  </UDashboardPanel>
</template>
```

Use the `left`, `default` and `right` slots to customize the navbar.

```vue [DashboardNavbarExample.vue]
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items: TabsItem[] = [{
  label: 'All',
  value: 'all'
}, {
  label: 'Unread',
  value: 'unread'
}]
</script>

<template>
  <UDashboardNavbar title="Inbox">
    <template #leading>
      <UDashboardSidebarCollapse />
    </template>

    <template #trailing>
      <UBadge label="4" variant="subtle" />
    </template>

    <template #right>
      <UTabs
        :items="items"
        default-value="all"
        size="sm"
        class="w-40"
        :content="false"
      />
    </template>
  </UDashboardNavbar>
</template>
```

\> \[!NOTE]
\> In this example, we use the \[Tabs]\(/docs/components/tabs) component in the right slot to display some tabs.

### Title

Use the `title` prop to set the title of the navbar.

```vue
<template>
  <UDashboardNavbar title="Dashboard" />
</template>
```

### Icon

Use the `icon` prop to set the icon of the navbar.

```vue
<template>
  <UDashboardNavbar title="Dashboard" icon="i-lucide-house" />
</template>
```

### Toggle

Use the `toggle` prop to customize the toggle button displayed on mobile that opens the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) component.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue [DashboardNavbarToggleExample.vue]
<template>
  <UDashboardNavbar
    title="Dashboard"
    :toggle="{
      color: 'primary',
      variant: 'subtle',
      class: 'rounded-full'
    }"
  />
</template>
```

### Toggle Side

Use the `toggle-side` prop to change the side of the toggle button. Defaults to `right`.

```vue [DashboardNavbarToggleSideExample.vue]
<template>
  <UDashboardNavbar
    title="Dashboard"
    toggle-side="right"
  />
</template>
```

## API

### Props

```ts
/**
 * Props for the DashboardNavbar component
 */
interface DashboardNavbarProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed next to the title.
   */
  icon?: any;
  title?: string | undefined;
  /**
   * Customize the toggle button to open the sidebar.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  toggle?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The side to render the toggle button on.
   * @default "\"left\""
   */
  toggleSide?: "left" | "right" | undefined;
  ui?: { root?: ClassNameValue; left?: ClassNameValue; icon?: ClassNameValue; title?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; toggle?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardNavbar component
 */
interface DashboardNavbarSlots {
  title(): any;
  leading(): any;
  trailing(): any;
  left(): any;
  default(): any;
  right(): any;
  toggle(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardNavbar: {
      slots: {
        root: 'h-(--ui-header-height) shrink-0 flex items-center justify-between border-b border-default px-4 sm:px-6 gap-1.5',
        left: 'flex items-center gap-1.5 min-w-0',
        icon: 'shrink-0 size-5 self-center me-1.5',
        title: 'flex items-center gap-1.5 font-semibold text-highlighted truncate',
        center: 'hidden lg:flex',
        right: 'flex items-center shrink-0 gap-1.5',
        toggle: ''
      },
      variants: {
        toggleSide: {
          left: {
            toggle: ''
          },
          right: {
            toggle: ''
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardNavbar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-navbar.ts).


# DashboardPanel

## Usage

The DashboardPanel component is used to display a panel. Its state (size, collapsed, etc.) will be saved based on the `storage` and `storage-key` props you provide to the [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group#props) component.

Use it inside the default slot of the [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group) component, you can put multiple panels next to each other:

```vue [pages/index.vue] {8,10}
<script setup lang="ts">
definePageMeta({
  layout: 'dashboard'
})
</script>

<template>
  <UDashboardPanel id="inbox-1" resizable />

  <UDashboardPanel id="inbox-2" class="hidden lg:flex" />
</template>
```

\> \[!CAUTION]
\> It is recommended to set an \`id\` when using multiple panels in different pages to avoid conflicts.

\> \[!WARNING]
\> This component does not have a single root element when using the \`resizable\` prop, so wrap it in a container (e.g., \`\<div class="flex flex-1">\`) if you use page transitions or require a single root for layout.

Use the `header`, `body` and `footer` slots to customize the panel or the default slot if you don't want a scrollable body with padding.

```vue [DashboardPanelExample.vue]
<template>
  <UDashboardPanel resizable>
    <template #header>
      <UDashboardNavbar title="Inbox">
        <template #leading>
          <UDashboardSidebarCollapse />
        </template>
      </UDashboardNavbar>
    </template>

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </UDashboardPanel>
</template>
```

\> \[!NOTE]
\> Most of the time, you will use the \[\`DashboardNavbar\`]\(/docs/components/dashboard-navbar) component in the \`header\` slot.

### Resizable

Use the `resizable` prop to make the panel resizable.

```vue
<template>
  <UDashboardPanel resizable>
    <template #body>
      <Placeholder class="h-96" />
    </template></UDashboardPanel>
</template>
```

### Size

Use the `min-size`, `max-size` and `default-size` props to customize the size of the panel.

```vue
<template>
  <UDashboardPanel resizable :min-size="22" :default-size="35" :max-size="40">
    <template #body>
      <Placeholder class="h-96" />
    </template></UDashboardPanel>
</template>
```

\> \[!TIP]
\> See: /docs/components/dashboard-group#props
\> Sizes are calculated as percentages by default. You can change this using the \`unit\` prop on the \`DashboardGroup\` component.

## API

### Props

```ts
/**
 * Props for the DashboardPanel component
 */
interface DashboardPanelProps {
  ui?: { root?: ClassNameValue; body?: ClassNameValue; handle?: ClassNameValue; } | undefined;
  /**
   * The id of the panel.
   */
  id?: string | undefined;
  /**
   * The minimum size of the panel.
   * @default "15"
   */
  minSize?: number | undefined;
  /**
   * The maximum size of the panel.
   */
  maxSize?: number | undefined;
  /**
   * The default size of the panel.
   */
  defaultSize?: number | undefined;
  /**
   * Whether to allow the user to resize the panel.
   * @default "false"
   */
  resizable?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardPanel component
 */
interface DashboardPanelSlots {
  default(): any;
  header(): any;
  body(): any;
  footer(): any;
  resize-handle(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardPanel: {
      slots: {
        root: 'relative flex flex-col min-w-0 min-h-svh lg:not-last:border-e lg:not-last:border-default shrink-0',
        body: 'flex flex-col gap-4 sm:gap-6 flex-1 overflow-y-auto p-4 sm:p-6',
        handle: ''
      },
      variants: {
        size: {
          true: {
            root: 'w-full lg:w-(--width)'
          },
          false: {
            root: 'flex-1'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardPanel.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-panel.ts).


# DashboardResizeHandle

## Usage

The DashboardResizeHandle component is used by the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) and [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) components.

It is automatically displayed when the `resizable` prop is set, **you don't have to add it manually**.

## Examples

### Within `resize-handle` slot

Even though this component is automatically displayed when the `resizable` prop is set, you can use the `resize-handle` slot of the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) and [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) components to customize the handle.

\`\`\`vue
\<template>
\<UDashboardGroup>
\<UDashboardSidebar resizable>
\<template #resize-handle="{ onMouseDown, onTouchStart, onDoubleClick }">
\<UDashboardResizeHandle
class="after\:absolute after\:inset-y-0 after\:right-0 after\:w-px hover\:after\:bg-(--ui-border-accented) after\:transition"
@mousedown="onMouseDown"
@touchstart="onTouchStart"
@dblclick="onDoubleClick"
/>
\</template>
\</UDashboardSidebar>
\<slot />
\</UDashboardGroup>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
definePageMeta({
layout: 'dashboard'
})
\</script>
\<template>
\<UDashboardPanel resizable>
\<template #resize-handle="{ onMouseDown, onTouchStart, onDoubleClick }">
\<UDashboardResizeHandle
class="after\:absolute after\:inset-y-0 after\:right-0 after\:w-px hover\:after\:bg-(--ui-border-accented) after\:transition"
@mousedown="onMouseDown"
@touchstart="onTouchStart"
@dblclick="onDoubleClick"
/>
\</template>
\</UDashboardPanel>
\</template>
\`\`\`

\> \[!NOTE]
\> In this example, we add an \`after\` pseudo-element to display a vertical line on hover.

## API

### Props

```ts
/**
 * Props for the DashboardResizeHandle component
 */
interface DashboardResizeHandleProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardResizeHandle component
 */
interface DashboardResizeHandleSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardResizeHandle: {
      base: 'hidden lg:block touch-none select-none cursor-ew-resize relative before:absolute before:inset-y-0 before:-left-1.5 before:-right-1.5 before:z-1'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardResizeHandle.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-resize-handle.ts).


# DashboardSearch

## Usage

The DashboardSearch component extends the [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) component, so you can pass any property such as `icon`, `placeholder`, etc.

Use it inside the default slot of the [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group) component:

```vue [layouts/dashboard.vue] {3}
<template>
  <UDashboardGroup>
    <UDashboardSidebar>
      <UDashboardSearchButton />
    </UDashboardSidebar>

    <UDashboardSearch />

    <slot />
  </UDashboardGroup>
</template>
```

\> \[!TIP]
\> You can open the CommandPalette by pressing , by using the \[DashboardSearchButton]\(/docs/components/dashboard-search-button) component or by using a \`v-model\:open\` directive.

### Shortcut

Use the `shortcut` prop to change the shortcut used in [defineShortcuts](https://ui.nuxt.com/docs/composables/define-shortcuts) to open the ContentSearch component. Defaults to `meta_k` ( :kbd{value="meta"} :kbd{value="K"} ).

```vue [app.vue] {4}
<template>
  <UDashboardSearch
    v-model:search-term="searchTerm"
    shortcut="meta_k"
    :groups="groups"
    :fuse="{ resultLimit: 42 }"
  />
</template>
```

### Color Mode

By default, a group of commands will be added to the command palette so you can switch between light and dark mode. This will only take effect if the `colorMode` is not forced in a specific page which can be achieved through `definePageMeta`:

```vue [pages/index.vue]
<script setup lang="ts">
definePageMeta({
  colorMode: 'dark'
})
</script>
```

You can disable this behavior by setting the `color-mode` prop to `false`:

```vue [app.vue] {4}
<template>
  <UDashboardSearch
    v-model:search-term="searchTerm"
    :color-mode="false"
    :groups="groups"
    :fuse="{ resultLimit: 42 }"
  />
</template>
```

## API

### Props

```ts
/**
 * Props for the DashboardSearch component
 */
interface DashboardSearchProps {
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  /**
   * The icon displayed in the input.
   */
  icon?: any;
  /**
   * The placeholder text for the input.
   */
  placeholder?: string | undefined;
  /**
   * Automatically focus the input when component is mounted.
   */
  autofocus?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * Display a close button in the input (useful when inside a Modal for example).
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * Keyboard shortcut to open the search (used by [`defineShortcuts`](https://ui.nuxt.com/docs/composables/define-shortcuts))
   * @default "\"meta_k\""
   */
  shortcut?: string | undefined;
  groups?: CommandPaletteGroup<CommandPaletteItem>[] | undefined;
  /**
   * Options for [useFuse](https://vueuse.org/integrations/useFuse) passed to the [CommandPalette](https://ui.nuxt.com/docs/components/command-palette).
   */
  fuse?: n<CommandPaletteItem> | undefined;
  /**
   * When `true`, the theme command will be added to the groups.
   * @default "true"
   */
  colorMode?: boolean | undefined;
  ui?: ({ modal?: ClassNameValue; input?: ClassNameValue; } & { root?: ClassNameValue; input?: ClassNameValue; close?: ClassNameValue; back?: ClassNameValue; content?: ClassNameValue; footer?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingHighlightedIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelBase?: ClassNameValue; itemLabelPrefix?: ClassNameValue; itemLabelSuffix?: ClassNameValue; }) | undefined;
  title?: string | undefined;
  /**
   * Animate the modal when opening or closing.
   */
  transition?: boolean | undefined;
  description?: string | undefined;
  /**
   * Render an overlay behind the modal.
   */
  overlay?: boolean | undefined;
  /**
   * The content of the modal.
   */
  content?: (Omit<DialogContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DialogContentImplEmits>>) | undefined;
  /**
   * When `false`, the modal will not close when clicking outside or pressing escape.
   */
  dismissible?: boolean | undefined;
  /**
   * When `true`, the modal will take up the full screen.
   * @default "false"
   */
  fullscreen?: boolean | undefined;
  /**
   * The modality of the dialog When set to `true`, <br>
   * interaction with outside elements will be disabled and only dialog content will be visible to screen readers.
   */
  modal?: boolean | undefined;
  /**
   * Render the modal in a portal.
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * @default "false"
   */
  open?: boolean | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardSearch component
 */
interface DashboardSearchSlots {
  empty(): any;
  footer(): any;
  back(): any;
  close(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the DashboardSearch component
 */
interface DashboardSearchEmits {
  update:open: (payload: [value: boolean]) => void;
  update:searchTerm: (payload: [value: string]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                                    | Type                                                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commandPaletteRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<InstanceType<typeof UCommandPalette> | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardSearch: {
      slots: {
        modal: '',
        input: ''
      },
      variants: {
        fullscreen: {
          false: {
            modal: 'sm:max-w-3xl h-full sm:h-[28rem]'
          }
        },
        size: {
          xs: {},
          sm: {},
          md: {},
          lg: {},
          xl: {}
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardSearch.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-search.ts).


# DashboardSearchButton

## Usage

The DashboardSearchButton component is used to open the [DashboardSearch](https://ui.nuxt.com/docs/components/dashboard-search) modal.

```vue
<template>
  <UDashboardSearchButton />
</template>
```

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UDashboardSearchButton variant="subtle" />
</template>
```

\> \[!NOTE]
\> See: #collapsed
\> The button defaults to \`color="neutral"\` and \`variant="outline"\` when not collapsed, \`variant="ghost"\` when collapsed.

### Collapsed

Use the `collapsed` prop to hide the button's label and [kbds](https://ui.nuxt.com/#kbds). Defaults to `false`.

```vue
<template>
  <UDashboardSearchButton collapsed />
</template>
```

\> \[!TIP]
\> See: /docs/components/dashboard-sidebar#slots
\> When using the button in the DashboardSidebar component, use the \`collapsed\` slot prop directly.

### Kbds

Use the `kbds` prop to display keyboard keys in the button. Defaults to `['meta', 'K']`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} to match the default shortcut of the [DashboardSearch](https://ui.nuxt.com/docs/components/dashboard-search#shortcut) component.

```vue
<template>
  <UDashboardSearchButton :collapsed="false" />
</template>
```

## API

### Props

```ts
/**
 * Props for the DashboardSearchButton component
 */
interface DashboardSearchButtonProps {
  /**
   * The icon displayed in the button.
   */
  icon?: any;
  /**
   * The label displayed in the button.
   */
  label?: string | undefined;
  /**
   * The color of the button.
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * The variant of the button.
   * Defaults to 'outline' when not collapsed, 'ghost' when collapsed.
   */
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * Whether the button is collapsed.
   * @default "false"
   */
  collapsed?: boolean | undefined;
  /**
   * Display a tooltip on the button when is collapsed with the button label.
   * This has priority over the global `tooltip` prop.
   * @default "false"
   */
  tooltip?: boolean | TooltipProps | undefined;
  /**
   * The keyboard keys to display in the button.
   * `{ variant: 'subtle' }`{lang="ts-type"}
   * @default "[\"meta\", \"k\"]"
   */
  kbds?: (string | undefined)[] | KbdProps[] | undefined;
  ui?: ({ base?: ClassNameValue; label?: ClassNameValue; trailing?: ClassNameValue; } & { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; }) | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  activeVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the DashboardSearchButton component
 */
interface DashboardSearchButtonSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardSearchButton: {
      slots: {
        base: '',
        label: '',
        trailing: 'hidden lg:flex items-center gap-0.5 ms-auto'
      },
      variants: {
        collapsed: {
          true: {
            label: 'hidden',
            trailing: 'lg:hidden'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardSearchButton.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-search-button.ts).


# DashboardSidebar

## Usage

The DashboardSidebar component is used to display a sidebar in a dashboard layout. It supports drag-to-resize, state persistence and integrates with [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group), [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) and [DashboardNavbar](https://ui.nuxt.com/docs/components/dashboard-navbar).

\> \[!TIP]
\> See: /docs/components/sidebar
\> DashboardSidebar vs Sidebar: This component is designed for dashboard layouts with drag-to-resize, state persistence and \`DashboardGroup\` integration. For a simple, standalone sidebar (chat panel, settings, navigation), use \[Sidebar]\(/docs/components/sidebar) instead.

Its state (size, collapsed, etc.) will be saved based on the `storage` and `storage-key` props you provide to the [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group#props) component.

Use it inside the default slot of the [DashboardGroup](https://ui.nuxt.com/docs/components/dashboard-group) component:

```vue [layouts/dashboard.vue] {3}
<template>
  <UDashboardGroup>
    <UDashboardSidebar />

    <slot />
  </UDashboardGroup>
</template>
```

\> \[!WARNING]
\> This component does not have a single root element when using the \`resizable\` prop, so wrap it in a container (e.g., \`\<div class="flex flex-1">\`) if you use page transitions or require a single root for layout.

Use the `header`, `default` and `footer` slots to customize the sidebar and the `body` or `content` slots to customize the sidebar menu.

```vue [DashboardSidebarExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[][] = [[{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}, {
  label: 'Settings',
  icon: 'i-lucide-settings',
  defaultOpen: true,
  children: [{
    label: 'General'
  }, {
    label: 'Members'
  }, {
    label: 'Notifications'
  }]
}], [{
  label: 'Feedback',
  icon: 'i-lucide-message-circle',
  to: 'https://github.com/nuxt-ui-templates/dashboard',
  target: '_blank'
}, {
  label: 'Help & Support',
  icon: 'i-lucide-info',
  to: 'https://github.com/nuxt/ui',
  target: '_blank'
}]]
</script>

<template>
  <UDashboardSidebar collapsible resizable :ui="{ footer: 'border-t border-default' }">
    <template #header="{ collapsed }">
      <Logo v-if="!collapsed" class="h-5 w-auto shrink-0" />
      <UIcon v-else name="i-simple-icons-nuxtdotjs" class="size-5 text-primary mx-auto" />
    </template>

    <template #default="{ collapsed }">
      <UButton
        :label="collapsed ? undefined : 'Search...'"
        icon="i-lucide-search"
        color="neutral"
        variant="outline"
        block
        :square="collapsed"
      >
        <template v-if="!collapsed" #trailing>
          <div class="flex items-center gap-0.5 ms-auto">
            <UKbd value="meta" variant="subtle" />
            <UKbd value="K" variant="subtle" />
          </div>
        </template>
      </UButton>

      <UNavigationMenu
        :collapsed="collapsed"
        :items="items[0]"
        orientation="vertical"
      />

      <UNavigationMenu
        :collapsed="collapsed"
        :items="items[1]"
        orientation="vertical"
        class="mt-auto"
      />
    </template>

    <template #footer="{ collapsed }">
      <UButton
        :avatar="{
          src: 'https://github.com/benjamincanac.png',
          loading: 'lazy' as const
        }"
        :label="collapsed ? undefined : 'Benjamin'"
        color="neutral"
        variant="ghost"
        class="w-full"
        :block="collapsed"
      />
    </template>
  </UDashboardSidebar>
</template>
```

\> \[!NOTE]
\> Drag the sidebar near the left edge of the screen to collapse it.

### Resizable

Use the `resizable` prop to make the sidebar resizable.

```vue
<template>
  <UDashboardSidebar resizable>
    <Placeholder class="h-96" />
  </UDashboardSidebar>
</template>
```

### Collapsible

Use the `collapsible` prop to make the sidebar collapsible when dragging near the edge of the screen.

\> \[!WARNING]
\> The \[\`DashboardSidebarCollapse\`]\(/docs/components/dashboard-sidebar-collapse) component will have no effect if the sidebar is not collapsible.

```vue
<template>
  <UDashboardSidebar resizable collapsible>
    <Placeholder class="h-96" />
  </UDashboardSidebar>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can access the \`collapsed\` state in the slot props to customize the content of the sidebar when it is collapsed.

### Size

Use the `min-size`, `max-size`, `default-size` and `collapsed-size` props to customize the size of the sidebar.

```vue
<template>
  <UDashboardSidebar resizable collapsible :min-size="22" :default-size="35" :max-size="40" :collapsed-size="0">
    <Placeholder class="h-96" />
  </UDashboardSidebar>
</template>
```

\> \[!TIP]
\> See: /docs/components/dashboard-group#props
\> Sizes are calculated as percentages by default. You can change this using the \`unit\` prop on the \`DashboardGroup\` component.

\> \[!NOTE]
\> The \`collapsed-size\` prop is set to \`0\` by default but the sidebar has a \`min-w-16\` to make sure it is visible.

### Side

Use the `side` prop to change the side of the sidebar. Defaults to `left`.

```vue
<template>
  <UDashboardSidebar side="right" resizable collapsible>
    <Placeholder class="h-96" />
  </UDashboardSidebar>
</template>
```

### Mode

Use the `mode` prop to change the mode of the sidebar menu. Defaults to `slideover`.

Use the `body` slot to fill the menu body (under the header) or the `content` slot to fill the entire menu.

\> \[!TIP]
\> See: #props
\> You can use the \`menu\` prop to customize the menu of the sidebar, it will adapt depending on the mode you choose.

```vue [DashboardSidebarModeExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

defineProps<{
  mode: 'drawer' | 'slideover' | 'modal'
}>()

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <UDashboardGroup>
    <UDashboardSidebar :mode="mode">
      <template #header="{ collapsed }">
        <Logo v-if="!collapsed" class="h-5 w-auto" />
        <UIcon v-else name="i-simple-icons-nuxtdotjs" class="size-5 text-primary mx-auto" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
      />
    </UDashboardSidebar>

    <UDashboardPanel>
      <template #header>
        <UDashboardNavbar title="Dashboard" />
      </template>
    </UDashboardPanel>
  </UDashboardGroup>
</template>
```

\> \[!NOTE]
\> These examples contain the \[\`DashboardGroup\`]\(/docs/components/dashboard-group), \[\`DashboardPanel\`]\(/docs/components/dashboard-panel) and \[\`DashboardNavbar\`]\(/docs/components/dashboard-navbar) components as they are required to demonstrate the sidebar on mobile.

### Toggle

Use the `toggle` prop to customize the [DashboardSidebarToggle](https://ui.nuxt.com/docs/components/dashboard-sidebar-toggle) component displayed on mobile.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue [DashboardSidebarToggleExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <UDashboardGroup>
    <UDashboardSidebar
      open
      :toggle="{
        color: 'primary',
        variant: 'subtle',
        class: 'rounded-full'
      }"
    >
      <template #header>
        <Logo class="h-5 w-auto" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
      />
    </UDashboardSidebar>

    <UDashboardPanel>
      <template #header>
        <UDashboardNavbar title="Dashboard" />
      </template>
    </UDashboardPanel>
  </UDashboardGroup>
</template>
```

### Toggle Side

Use the `toggle-side` prop to change the side of the toggle button. Defaults to `left`.

```vue [DashboardSidebarToggleSideExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <UDashboardGroup>
    <UDashboardSidebar
      open
      toggle-side="right"
    >
      <template #header>
        <Logo class="h-5 w-auto" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
      />
    </UDashboardSidebar>

    <UDashboardPanel>
      <template #header>
        <UDashboardNavbar title="Dashboard" />
      </template>
    </UDashboardPanel>
  </UDashboardGroup>
</template>
```

## Examples

### Control open state

You can control the open state by using the `open` prop or the `v-model:open` directive.

```vue [DashboardSidebarOpenExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]

const open = ref(true)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UDashboardSidebar v-model:open="open">
    <template #header="{ collapsed }">
      <Logo v-if="!collapsed" class="h-5 w-auto" />
      <UIcon v-else name="i-simple-icons-nuxtdotjs" class="size-5 text-primary mx-auto" />
    </template>

    <UNavigationMenu
      :items="items"
      orientation="vertical"
    />
  </UDashboardSidebar>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the open state of the DashboardSidebar by pressing .

### Control collapsed state

You can control the collapsed state by using the `collapsed` prop or the `v-model:collapsed` directive.

```vue [DashboardSidebarCollapsedExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]

const collapsed = ref(false)

defineShortcuts({
  c: () => collapsed.value = !collapsed.value
})
</script>

<template>
  <UDashboardSidebar v-model:collapsed="collapsed" collapsible>
    <template #header>
      <Logo v-if="!collapsed" class="h-5 w-auto" />
      <UIcon v-else name="i-simple-icons-nuxtdotjs" class="size-5 text-primary mx-auto" />
    </template>

    <UNavigationMenu
      :collapsed="collapsed"
      :items="items"
      orientation="vertical"
    />
  </UDashboardSidebar>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the collapsed state of the DashboardSidebar by pressing .

## API

### Props

```ts
/**
 * Props for the DashboardSidebar component
 */
interface DashboardSidebarProps {
  /**
   * The mode of the sidebar menu.
   * @default "\"slideover\" as never"
   */
  mode?: T | undefined;
  /**
   * The props for the sidebar menu component.
   */
  menu?: DashboardSidebarMenu<T> | undefined;
  /**
   * Customize the toggle button to open the sidebar.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  toggle?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The side to render the toggle button on.
   * @default "\"left\""
   */
  toggleSide?: "left" | "right" | undefined;
  /**
   * Automatically close when route changes.
   * @default "true"
   */
  autoClose?: boolean | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; toggle?: ClassNameValue; handle?: ClassNameValue; content?: ClassNameValue; overlay?: ClassNameValue; } | undefined;
  /**
   * The id of the panel.
   */
  id?: string | undefined;
  /**
   * The side to render the panel on.
   * @default "\"left\""
   */
  side?: "left" | "right" | undefined;
  /**
   * The minimum size of the panel.
   * @default "10"
   */
  minSize?: number | undefined;
  /**
   * The maximum size of the panel.
   * @default "20"
   */
  maxSize?: number | undefined;
  /**
   * The default size of the panel.
   * @default "15"
   */
  defaultSize?: number | undefined;
  /**
   * Whether to allow the user to resize the panel.
   * @default "false"
   */
  resizable?: boolean | undefined;
  /**
   * Whether to allow the user to collapse the panel.
   * @default "false"
   */
  collapsible?: boolean | undefined;
  /**
   * The size of the panel when collapsed.
   * @default "0"
   */
  collapsedSize?: number | undefined;
  /**
   * @default "false"
   */
  open?: boolean | undefined;
  /**
   * @default "false"
   */
  collapsed?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardSidebar component
 */
interface DashboardSidebarSlots {
  header(): any;
  default(): any;
  footer(): any;
  toggle(): any;
  content(): any;
  resize-handle(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardSidebar: {
      slots: {
        root: 'relative hidden lg:flex flex-col min-h-svh min-w-16 w-(--width) shrink-0',
        header: 'h-(--ui-header-height) shrink-0 flex items-center gap-1.5 px-4',
        body: 'flex flex-col gap-4 flex-1 overflow-y-auto px-4 py-2',
        footer: 'shrink-0 flex items-center gap-1.5 px-4 py-2',
        toggle: '',
        handle: '',
        content: 'lg:hidden',
        overlay: 'lg:hidden'
      },
      variants: {
        menu: {
          true: {
            header: 'sm:px-6',
            body: 'sm:px-6',
            footer: 'sm:px-6'
          }
        },
        side: {
          left: {
            root: 'border-e border-default'
          },
          right: {
            root: ''
          }
        },
        toggleSide: {
          left: {
            toggle: ''
          },
          right: {
            toggle: 'ms-auto'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardSidebar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-sidebar.ts).


# DashboardSidebarCollapse

## Usage

The DashboardSidebarCollapse component is used to collapse/expand the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) component **when its `collapsible` prop is set**.

```vue
<template>
  <UDashboardSidebarCollapse />
</template>
```

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UDashboardSidebarCollapse variant="subtle" />
</template>
```

\> \[!NOTE]
\> The button defaults to \`color="neutral"\` and \`variant="ghost"\`.

## Examples

### Within `header` slot

You can put this component in the `header` slot of the [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) component and use the `collapsed` prop to hide the left part of the header for example:

```vue [layouts/dashboard.vue] {4-8}
<template>
  <UDashboardGroup>
    <UDashboardSidebar collapsible>
      <template #header="{ collapsed }">
        <Logo v-if="!collapsed" />

        <UDashboardSidebarCollapse variant="subtle" />
      </template>
    </UDashboardSidebar>

    <slot />
  </UDashboardGroup>
</template>
```

### Within `leading` slot

You can put this component in the `leading` slot of the [DashboardNavbar](https://ui.nuxt.com/docs/components/dashboard-navbar) component to display it before the title for example:

```vue [pages/index.vue] {11-13}
<script setup lang="ts">
definePageMeta({
  layout: 'dashboard'
})
</script>

<template>
  <UDashboardPanel>
    <template #header>
      <UDashboardNavbar title="Home">
        <template #leading>
          <UDashboardSidebarCollapse variant="subtle" />
        </template>
      </UDashboardNavbar>
    </template>
  </UDashboardPanel>
</template>
```

## API

### Props

```ts
/**
 * Props for the DashboardSidebarCollapse component
 */
interface DashboardSidebarCollapseProps {
  /**
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * @default "\"ghost\""
   */
  variant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  /**
   * The side of the sidebar to collapse.
   * @default "\"left\""
   */
  side?: "left" | "right" | undefined;
  ui?: { base?: any; } | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  label?: string | undefined;
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  activeVariant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardSidebarCollapse: {
      base: 'hidden lg:flex',
      variants: {
        side: {
          left: '',
          right: ''
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardSidebarCollapse.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-sidebar-collapse.ts).


# DashboardSidebarToggle

## Usage

The DashboardSidebarToggle component is used by the [DashboardNavbar](https://ui.nuxt.com/docs/components/dashboard-navbar) and [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) components.

It is automatically displayed on mobile to toggle the sidebar, **you don't have to add it manually**.

```vue
<template>
  <UDashboardSidebarToggle />
</template>
```

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue
<template>
  <UDashboardSidebarToggle variant="subtle" />
</template>
```

\> \[!NOTE]
\> The button defaults to \`color="neutral"\` and \`variant="ghost"\`.

## Examples

### Within `toggle` slot

Even though this component is automatically displayed on mobile, you can use the `toggle` slot of the [DashboardNavbar](https://ui.nuxt.com/docs/components/dashboard-navbar) and [DashboardSidebar](https://ui.nuxt.com/docs/components/dashboard-sidebar) components to customize the button.

\`\`\`vue
\<template>
\<UDashboardGroup>
\<UDashboardSidebar>
\<template #toggle>
\<UDashboardSidebarToggle variant="subtle" />
\</template>
\</UDashboardSidebar>
\<slot />
\</UDashboardGroup>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
definePageMeta({
layout: 'dashboard'
})
\</script>
\<template>
\<UDashboardPanel>
\<template #header>
\<UDashboardNavbar title="Home">
\<template #toggle>
\<UDashboardSidebarToggle variant="subtle" />
\</template>
\</UDashboardNavbar>
\</template>
\</UDashboardPanel>
\</template>
\`\`\`

\> \[!TIP]
\> When using the \`toggle-side\` prop of the \`DashboardSidebar\` and \`DashboardNavbar\` components, the button will be displayed on the specified side.

## API

### Props

```ts
/**
 * Props for the DashboardSidebarToggle component
 */
interface DashboardSidebarToggleProps {
  /**
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * @default "\"ghost\""
   */
  variant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  /**
   * The side of the sidebar to toggle.
   * @default "\"left\""
   */
  side?: "left" | "right" | undefined;
  ui?: { base?: any; } | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  label?: string | undefined;
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  activeVariant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardSidebarToggle: {
      base: 'lg:hidden',
      variants: {
        side: {
          left: '',
          right: ''
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardSidebarToggle.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-sidebar-toggle.ts).


# DashboardToolbar

## Usage

The DashboardToolbar component is used to display a toolbar under the [DashboardNavbar](https://ui.nuxt.com/docs/components/dashboard-navbar) component.

Use it inside the `header` slot of the [DashboardPanel](https://ui.nuxt.com/docs/components/dashboard-panel) component:

```vue [pages/index.vue] {9-13}
<script setup lang="ts">
definePageMeta({
  layout: 'dashboard'
})
</script>

<template>
  <UDashboardPanel>
    <template #header>
      <UDashboardNavbar />

      <UDashboardToolbar />
    </template>
  </UDashboardPanel>
</template>
```

Use the `left`, `default` and `right` slots to customize the toolbar.

```vue [DashboardToolbarExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[][] = [[{
  label: 'General',
  icon: 'i-lucide-user',
  active: true
}, {
  label: 'Members',
  icon: 'i-lucide-users'
}, {
  label: 'Notifications',
  icon: 'i-lucide-bell'
}], [{
  label: 'Documentation',
  icon: 'i-lucide-book-open',
  to: 'https://ui.nuxt.com/docs',
  target: '_blank'
}, {
  label: 'Help & Feedback',
  icon: 'i-lucide-help-circle',
  to: 'https://github.com/nuxt/ui/issues',
  target: '_blank'
}]]
</script>

<template>
  <UDashboardToolbar>
    <UNavigationMenu :items="items" highlight class="flex-1" />
  </UDashboardToolbar>
</template>
```

\> \[!NOTE]
\> In this example, we use the \[NavigationMenu]\(/docs/components/navigation-menu) component to render some links.

## API

### Props

```ts
/**
 * Props for the DashboardToolbar component
 */
interface DashboardToolbarProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { root?: ClassNameValue; left?: ClassNameValue; right?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DashboardToolbar component
 */
interface DashboardToolbarSlots {
  default(): any;
  left(): any;
  right(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dashboardToolbar: {
      slots: {
        root: 'shrink-0 flex items-center justify-between border-b border-default px-4 sm:px-6 gap-1.5 overflow-x-auto min-h-[49px]',
        left: 'flex items-center gap-1.5',
        right: 'flex items-center gap-1.5'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DashboardToolbar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dashboard-toolbar.ts).


# Drawer

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Drawer.

Then, use the `#content` slot to add the content displayed when the Drawer is open.

```vue
<template>
  <UDrawer>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

You can also use the `#header`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, `#body`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} and `#footer`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} slots to customize the Drawer's content.

### Title

Use the `title` prop to set the title of the Drawer's header.

```vue
<template>
  <UDrawer title="Drawer with title">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UDrawer>
</template>
```

### Description

Use the `description` prop to set the description of the Drawer's header.

```vue
<template>
  <UDrawer title="Drawer with description" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UDrawer>
</template>
```

### Direction

Use the `direction` prop to control the direction of the Drawer. Defaults to `bottom`.

```vue
<template>
  <UDrawer direction="right">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="min-w-96 min-h-96 size-full m-4" />
    </template></UDrawer>
</template>
```

### Inset

Use the `inset` prop to inset the Drawer from the edges.

```vue
<template>
  <UDrawer direction="right" inset>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="min-w-96 min-h-96 size-full m-4" />
    </template></UDrawer>
</template>
```

### Handle

Use the `handle` prop to control whether the Drawer has a handle or not. Defaults to `true`.

```vue
<template>
  <UDrawer :handle="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

### Handle Only

Use the `handle-only` prop to only allow the Drawer to be dragged by the handle.

```vue
<template>
  <UDrawer handle-only>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

### Overlay

Use the `overlay` prop to control whether the Drawer has an overlay or not. Defaults to `true`.

```vue
<template>
  <UDrawer :overlay="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

### Modal

Use the `modal` prop to control whether the Drawer blocks interaction with outside content. Defaults to `true`.

\> \[!NOTE]
\> When \`modal\` is set to \`false\`, the overlay is automatically disabled and outside content becomes interactive.

```vue
<template>
  <UDrawer :modal="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

### Dismissible

Use the `dismissible` prop to control whether the Drawer is dismissible when clicking outside of it or pressing escape. Defaults to `true`.

\> \[!NOTE]
\> A \`close\:prevent\` event will be emitted when the user tries to close it.

\> \[!TIP]
\> You can combine \`modal: false\` with \`dismissible: false\` to make the Drawer's background interactive without closing it.

```vue [DrawerDismissibleExample.vue]
<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <UDrawer v-model:open="open" :dismissible="false" :modal="false" :handle="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <div class="flex items-center justify-between gap-4 mb-4">
        <h2 class="text-highlighted font-semibold">
          Drawer non-dismissible
        </h2>

        <UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="open = false" />
      </div>

      <Placeholder class="size-full min-h-48" />
    </template>
  </UDrawer>
</template>
```

### Scale Background

Use the `should-scale-background` prop to scale the background when the Drawer is open, creating a visual depth effect. You can set the `set-background-color-on-scale` prop to `false` to prevent changing the background color.

```vue
<template>
  <UDrawer should-scale-background set-background-color-on-scale>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UDrawer>
</template>
```

\> \[!WARNING]
\> Make sure to add the \`data-vaul-drawer-wrapper\` directive to a parent element of your app to make this work.
\> \`\`\`vue
\> \<template>
\> \<UApp>
\> \<div class="bg-default" data-vaul-drawer-wrapper>
\> \<NuxtLayout>
\> \<NuxtPage />
\> \</NuxtLayout>
\> \</div>
\> \</UApp>
\> \</template>
\>
\> \`\`\`
\>
\> \`\`\`ts
\> export default defineNuxtConfig({
\> app: {
\> rootAttrs: {
\> 'data-vaul-drawer-wrapper': '',
\> 'class': 'bg-default'
\> }
\> }
\> })
\>
\> \`\`\`

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [DrawerOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UDrawer v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Drawer by pressing .

\> \[!TIP]
\> This allows you to move the trigger outside of the Drawer or remove it entirely.

### Responsive drawer

You can render a [Modal](https://ui.nuxt.com/docs/components/modal) component on desktop and a Drawer on mobile for example.

```vue [DrawerResponsiveExample.vue]
<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'

const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 768px)')

const open = ref(false)

const state = reactive({
  email: undefined
})

const title = 'Edit profile'
const description = 'Make changes to your profile here. Click save when you\'re done.'
</script>

<template>
  <DefineFormTemplate>
    <UForm :state="state" class="space-y-4">
      <UFormField label="Email" name="email" required>
        <UInput v-model="state.email" placeholder="shadcn@example.com" required />
      </UFormField>

      <UButton label="Save changes" type="submit" />
    </UForm>
  </DefineFormTemplate>

  <UModal v-if="isDesktop" v-model:open="open" :title="title" :description="description">
    <UButton label="Edit profile" color="neutral" variant="outline" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </UModal>

  <UDrawer v-else v-model:open="open" :title="title" :description="description">
    <UButton label="Edit profile" color="neutral" variant="outline" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </UDrawer>
</template>
```

### Nested drawers

You can nest drawers within each other by using the `nested` prop.

```vue [DrawerNestedExample.vue]
<template>
  <UDrawer :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #footer>
      <UDrawer nested :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
        <UButton color="neutral" variant="outline" label="Open nested" />

        <template #content>
          <Placeholder class="flex-1 m-4" />
        </template>
      </UDrawer>
    </template>
  </UDrawer>
</template>
```

### With footer slot

Use the `#footer` slot to add content after the Drawer's body.

```vue [DrawerFooterSlotExample.vue]
<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <UDrawer v-model:open="open" title="Drawer with footer" description="This is useful when you want a form in a Drawer." :ui="{ container: 'max-w-xl mx-auto' }">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer>
      <UButton label="Submit" color="neutral" class="justify-center" />
      <UButton label="Cancel" color="neutral" variant="outline" class="justify-center" @click="open = false" />
    </template>
  </UDrawer>
</template>
```

### With command palette

You can use a [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) component inside the Drawer's content.

```vue [DrawerCommandPaletteExample.vue]
<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'drawer-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UDrawer :handle="false" @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UDrawer>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the Drawer opens.

## API

### Props

```ts
/**
 * Props for the Drawer component
 */
interface DrawerProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Whether to inset the drawer from the edges.
   */
  inset?: boolean | undefined;
  /**
   * The content of the drawer.
   */
  content?: (Omit<DialogContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DialogContentImplEmits>>) | undefined;
  /**
   * Render an overlay behind the drawer.
   * @default "true"
   */
  overlay?: boolean | undefined;
  /**
   * Render a handle on the drawer.
   * @default "true"
   */
  handle?: boolean | undefined;
  /**
   * Render the drawer in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Whether the drawer is nested in another drawer.
   */
  nested?: boolean | undefined;
  ui?: { overlay?: ClassNameValue; content?: ClassNameValue; handle?: ClassNameValue; container?: ClassNameValue; header?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; } | undefined;
  /**
   * When `false` it allows to interact with elements outside of the drawer without closing it.
   * @default "true"
   */
  modal?: boolean | undefined;
  open?: boolean | undefined;
  activeSnapPoint?: string | number | null | undefined;
  /**
   * Number between 0 and 1 that determines when the drawer should be closed.
   * Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more.
   */
  closeThreshold?: number | undefined;
  shouldScaleBackground?: boolean | undefined;
  /**
   * When `false` we don't change body's background color when the drawer is open.
   */
  setBackgroundColorOnScale?: boolean | undefined;
  /**
   * Duration for which the drawer is not draggable after scrolling content inside of the drawer.
   */
  scrollLockTimeout?: number | undefined;
  /**
   * When `true`, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open
   */
  fixed?: boolean | undefined;
  /**
   * When `false` dragging, clicking outside, pressing esc, etc. will not close the drawer.
   * Use this in combination with the `open` prop, otherwise you won't be able to open/close the drawer.
   * @default "true"
   */
  dismissible?: boolean | undefined;
  /**
   * Opened by default, skips initial enter animation. Still reacts to `open` state changes
   */
  defaultOpen?: boolean | undefined;
  /**
   * Direction of the drawer. Can be `top` or `bottom`, `left`, `right`.
   * @default "\"bottom\""
   */
  direction?: DrawerDirection | undefined;
  /**
   * When `true` the `body` doesn't get any styles assigned from Vaul
   */
  noBodyStyles?: boolean | undefined;
  /**
   * When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component.
   */
  handleOnly?: boolean | undefined;
  preventScrollRestoration?: boolean | undefined;
  /**
   * Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up.
   * Should go from least visible. Example `[0.2, 0.5, 0.8]`.
   * You can also use px values, which doesn't take screen height into account.
   */
  snapPoints?: (string | number)[] | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Drawer component
 */
interface DrawerSlots {
  default(): any;
  content(): any;
  header(): any;
  title(): any;
  description(): any;
  body(): any;
  footer(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Drawer component
 */
interface DrawerEmits {
  update:open: (payload: [open: boolean]) => void;
  drag: (payload: [percentageDragged: number]) => void;
  close: (payload: []) => void;
  close:prevent: (payload: []) => void;
  release: (payload: [open: boolean]) => void;
  update:activeSnapPoint: (payload: [val: string | number]) => void;
  animationEnd: (payload: [open: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    drawer: {
      slots: {
        overlay: 'fixed inset-0 bg-elevated/75',
        content: 'fixed bg-default ring ring-default flex focus:outline-none',
        handle: [
          'shrink-0 !bg-accented',
          'transition-opacity'
        ],
        container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
        header: '',
        title: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        body: 'flex-1',
        footer: 'flex flex-col gap-1.5'
      },
      variants: {
        direction: {
          top: {
            content: 'mb-24 flex-col-reverse',
            handle: 'mb-4'
          },
          right: {
            content: 'flex-row',
            handle: '!ml-4'
          },
          bottom: {
            content: 'mt-24 flex-col',
            handle: 'mt-4'
          },
          left: {
            content: 'flex-row-reverse',
            handle: '!mr-4'
          }
        },
        inset: {
          true: {
            content: 'rounded-lg after:hidden overflow-hidden [--initial-transform:calc(100%+1.5rem)]'
          }
        },
        snapPoints: {
          true: ''
        }
      },
      compoundVariants: [
        {
          direction: [
            'top',
            'bottom'
          ],
          class: {
            content: 'h-auto max-h-[96%]',
            handle: '!w-12 !h-1.5 mx-auto'
          }
        },
        {
          direction: [
            'top',
            'bottom'
          ],
          snapPoints: true,
          class: {
            content: 'h-full'
          }
        },
        {
          direction: [
            'right',
            'left'
          ],
          class: {
            content: 'w-auto max-w-[calc(100%-2rem)]',
            handle: '!h-12 !w-1.5 mt-auto mb-auto'
          }
        },
        {
          direction: [
            'right',
            'left'
          ],
          snapPoints: true,
          class: {
            content: 'w-full'
          }
        },
        {
          direction: 'top',
          inset: true,
          class: {
            content: 'inset-x-4 top-4'
          }
        },
        {
          direction: 'top',
          inset: false,
          class: {
            content: 'inset-x-0 top-0 rounded-b-lg'
          }
        },
        {
          direction: 'bottom',
          inset: true,
          class: {
            content: 'inset-x-4 bottom-4'
          }
        },
        {
          direction: 'bottom',
          inset: false,
          class: {
            content: 'inset-x-0 bottom-0 rounded-t-lg'
          }
        },
        {
          direction: 'left',
          inset: true,
          class: {
            content: 'inset-y-4 left-4'
          }
        },
        {
          direction: 'left',
          inset: false,
          class: {
            content: 'inset-y-0 left-0 rounded-r-lg'
          }
        },
        {
          direction: 'right',
          inset: true,
          class: {
            content: 'inset-y-4 right-4'
          }
        },
        {
          direction: 'right',
          inset: false,
          class: {
            content: 'inset-y-0 right-0 rounded-l-lg'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Drawer.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/drawer.ts).


# DropdownMenu

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the DropdownMenu.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[][]>([
  [
    {
      label: 'Benjamin',
      avatar: {
        src: 'https://github.com/benjamincanac.png',
        loading: 'lazy',
      },
      type: 'label',
    },
  ],
  [
    {
      label: 'Profile',
      icon: 'i-lucide-user',
    },
    {
      label: 'Billing',
      icon: 'i-lucide-credit-card',
    },
    {
      label: 'Settings',
      icon: 'i-lucide-cog',
      kbds: [
        ',',
      ],
    },
    {
      label: 'Keyboard shortcuts',
      icon: 'i-lucide-monitor',
    },
  ],
  [
    {
      label: 'Team',
      icon: 'i-lucide-users',
      filter: {
        placeholder: 'Search members...',
      },
      children: [
        [
          {
            label: 'benjamincanac',
            avatar: {
              src: 'https://github.com/benjamincanac.png',
              loading: 'lazy',
            },
          },
          {
            label: 'HugoRCD',
            avatar: {
              src: 'https://github.com/HugoRCD.png',
              loading: 'lazy',
            },
          },
          {
            label: 'romhml',
            avatar: {
              src: 'https://github.com/romhml.png',
              loading: 'lazy',
            },
          },
          {
            label: 'sandros94',
            avatar: {
              src: 'https://github.com/sandros94.png',
              loading: 'lazy',
            },
          },
          {
            label: 'hywax',
            avatar: {
              src: 'https://github.com/hywax.png',
              loading: 'lazy',
            },
          },
          {
            label: 'J-Michalek',
            avatar: {
              src: 'https://github.com/J-Michalek.png',
              loading: 'lazy',
            },
          },
          {
            label: 'genu',
            avatar: {
              src: 'https://github.com/genu.png',
              loading: 'lazy',
            },
          },
        ],
      ],
    },
    {
      label: 'Invite users',
      icon: 'i-lucide-user-plus',
      children: [
        [
          {
            label: 'Email',
            icon: 'i-lucide-mail',
          },
          {
            label: 'Message',
            icon: 'i-lucide-message-square',
          },
        ],
        [
          {
            label: 'More',
            icon: 'i-lucide-circle-plus',
            children: [
              {
                label: 'Import from Slack',
                icon: 'i-simple-icons-slack',
                to: 'https://slack.com',
                target: '_blank',
              },
              {
                label: 'Import from Trello',
                icon: 'i-simple-icons-trello',
              },
              {
                label: 'Import from Asana',
                icon: 'i-simple-icons-asana',
              },
            ],
          },
        ],
      ],
    },
    {
      label: 'New team',
      icon: 'i-lucide-plus',
      kbds: [
        'meta',
        'n',
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Support',
      icon: 'i-lucide-life-buoy',
      to: '/docs/components/dropdown-menu',
    },
    {
      label: 'API',
      icon: 'i-lucide-cloud',
      disabled: true,
    },
  ],
  [
    {
      label: 'Logout',
      icon: 'i-lucide-log-out',
      color: 'error',
      kbds: [
        'shift',
        'meta',
        'q',
      ],
    },
  ],
])
</script>

<template>
  <UDropdownMenu :items="items">
    <UButton icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `kbds?: string[] | KbdProps[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`type?: "link" | "label" | "separator" | "checkbox"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-color-items)
- [`checked?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`onUpdateChecked?: (checked: boolean) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-checkbox-items)
- `children?: DropdownMenuItem[] | DropdownMenuItem[][]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`filter?: boolean | InputProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-filter-items)
- `filterFields?: string[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ignoreFilter?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[][]>([
  [
    {
      label: 'Benjamin',
      avatar: {
        src: 'https://github.com/benjamincanac.png',
        loading: 'lazy',
      },
      type: 'label',
    },
  ],
  [
    {
      label: 'Profile',
      icon: 'i-lucide-user',
    },
    {
      label: 'Billing',
      icon: 'i-lucide-credit-card',
    },
    {
      label: 'Settings',
      icon: 'i-lucide-cog',
      kbds: [
        ',',
      ],
    },
    {
      label: 'Keyboard shortcuts',
      icon: 'i-lucide-monitor',
    },
  ],
  [
    {
      label: 'Team',
      icon: 'i-lucide-users',
    },
    {
      label: 'Invite users',
      icon: 'i-lucide-user-plus',
      children: [
        [
          {
            label: 'Email',
            icon: 'i-lucide-mail',
          },
          {
            label: 'Message',
            icon: 'i-lucide-message-square',
          },
        ],
        [
          {
            label: 'More',
            icon: 'i-lucide-circle-plus',
            children: [
              {
                label: 'Import from Slack',
                icon: 'i-simple-icons-slack',
                to: 'https://slack.com',
                target: '_blank',
              },
              {
                label: 'Import from Trello',
                icon: 'i-simple-icons-trello',
              },
              {
                label: 'Import from Asana',
                icon: 'i-simple-icons-asana',
              },
            ],
          },
        ],
      ],
    },
    {
      label: 'New team',
      icon: 'i-lucide-plus',
      kbds: [
        'meta',
        'n',
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Support',
      icon: 'i-lucide-life-buoy',
      to: '/docs/components/dropdown-menu',
    },
    {
      label: 'API',
      icon: 'i-lucide-cloud',
      disabled: true,
    },
  ],
  [
    {
      label: 'Logout',
      icon: 'i-lucide-log-out',
      kbds: [
        'shift',
        'meta',
        'q',
      ],
    },
  ],
])
</script>

<template>
  <UDropdownMenu :items="items">
    <UButton icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

\> \[!TIP]
\> Each item can take a \`children\` array of objects with the same properties as the \`items\` prop to create a nested menu which can be controlled using the \`open\`, \`defaultOpen\` and \`content\` properties.

### Content

Use the `content` prop to control how the DropdownMenu content is rendered, like its `align` or `side` for example.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
])
</script>

<template>
  <UDropdownMenu :items="items">
    <UButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

### Filter `Soon`

Use the `filter` prop to display a filter input inside the DropdownMenu. Defaults to `false`.

\> \[!NOTE]
\> See: #with-ignore-filter
\> Use the \`ignore-filter\` prop to disable the internal search and use your own search logic.

\> \[!NOTE]
\> See: #with-filter-fields
\> Use the \`filter-fields\` prop to specify which fields to filter by. By default, it uses the \`labelKey\` prop.

You can pass any property from the [Input](https://ui.nuxt.com/docs/components/input) component to customize it.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
  {
    label: 'Team',
    icon: 'i-lucide-users',
  },
  {
    label: 'Invite users',
    icon: 'i-lucide-user-plus',
  },
  {
    label: 'New team',
    icon: 'i-lucide-plus',
  },
])
</script>

<template>
  <UDropdownMenu :items="items">
    <UButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

\> \[!TIP]
\> See: #with-filter-items
\> You can also enable the filter on specific sub-menus using the \`filter\` field on items with \`children\`.

### Arrow

Use the `arrow` prop to display an arrow on the DropdownMenu.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
])
</script>

<template>
  <UDropdownMenu arrow :items="items">
    <UButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

### Size

Use the `size` prop to control the size of the DropdownMenu.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
])
</script>

<template>
  <UDropdownMenu size="xl" :items="items">
    <UButton size="xl" label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

\> \[!WARNING]
\> The \`size\` prop will not be proxied to the Button, you need to set it yourself.

\> \[!NOTE]
\> When using the same size, the DropdownMenu items will be perfectly aligned with the Button.

### Modal

Use the `modal` prop to control whether the DropdownMenu blocks interaction with outside content. Defaults to `true`.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
])
</script>

<template>
  <UDropdownMenu :modal="false" :items="items">
    <UButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

### Disabled

Use the `disabled` prop to disable the DropdownMenu.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = ref<DropdownMenuItem[]>([
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
])
</script>

<template>
  <UDropdownMenu disabled :items="items">
    <UButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
  </UDropdownMenu>
</template>
```

## Examples

### With checkbox items

You can use the `type` property with `checkbox` and use the `checked` / `onUpdateChecked` properties to control the checked state of the item.

```vue [DropdownMenuCheckboxItemsExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const showBookmarks = ref(true)
const showHistory = ref(false)
const showDownloads = ref(false)

const items = computed(() => [{
  label: 'Interface',
  icon: 'i-lucide-app-window',
  type: 'label' as const
}, {
  type: 'separator' as const
}, {
  label: 'Show Bookmarks',
  icon: 'i-lucide-bookmark',
  type: 'checkbox' as const,
  checked: showBookmarks.value,
  onUpdateChecked(checked: boolean) {
    showBookmarks.value = checked
  },
  onSelect(e: Event) {
    e.preventDefault()
  }
}, {
  label: 'Show History',
  icon: 'i-lucide-clock',
  type: 'checkbox' as const,
  checked: showHistory.value,
  onUpdateChecked(checked: boolean) {
    showHistory.value = checked
  }
}, {
  label: 'Show Downloads',
  icon: 'i-lucide-download',
  type: 'checkbox' as const,
  checked: showDownloads.value,
  onUpdateChecked(checked: boolean) {
    showDownloads.value = checked
  }
}] satisfies DropdownMenuItem[])
</script>

<template>
  <UDropdownMenu :items="items" :content="{ align: 'start' }" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

\> \[!NOTE]
\> To ensure reactivity for the \`checked\` state of items, it's recommended to wrap your \`items\` array inside a \`computed\`.

### With color items

You can use the `color` property to highlight certain items with a color.

```vue [DropdownMenuColorItemsExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[][] = [
  [
    {
      label: 'View',
      icon: 'i-lucide-eye'
    },
    {
      label: 'Copy',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Edit',
      icon: 'i-lucide-pencil'
    }
  ],
  [
    {
      label: 'Delete',
      color: 'error',
      icon: 'i-lucide-trash'
    }
  ]
]
</script>

<template>
  <UDropdownMenu :items="items" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

### With filter items `Soon`

You can use the `filter` property on items with `children` to display a filter input inside the sub-menu.

```vue [DropdownMenuFilterItemsExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[][] = [[{
  label: 'Profile',
  icon: 'i-lucide-user'
}, {
  label: 'Billing',
  icon: 'i-lucide-credit-card'
}, {
  label: 'Settings',
  icon: 'i-lucide-cog'
}], [{
  label: 'Team',
  icon: 'i-lucide-users',
  filter: {
    placeholder: 'Search members...'
  },
  children: [
    {
      label: 'benjamincanac',
      avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const }
    },
    {
      label: 'HugoRCD',
      avatar: { src: 'https://github.com/HugoRCD.png', loading: 'lazy' as const }
    },
    {
      label: 'romhml',
      avatar: { src: 'https://github.com/romhml.png', loading: 'lazy' as const }
    },
    {
      label: 'sandros94',
      avatar: { src: 'https://github.com/sandros94.png', loading: 'lazy' as const }
    },
    {
      label: 'hywax',
      avatar: { src: 'https://github.com/hywax.png', loading: 'lazy' as const }
    },
    {
      label: 'J-Michalek',
      avatar: { src: 'https://github.com/J-Michalek.png', loading: 'lazy' as const }
    },
    {
      label: 'genu',
      avatar: { src: 'https://github.com/genu.png', loading: 'lazy' as const }
    }
  ]
}, {
  label: 'Invite users',
  icon: 'i-lucide-user-plus',
  children: [{
    label: 'Invite by email',
    icon: 'i-lucide-mail'
  }, {
    label: 'Invite by link',
    icon: 'i-lucide-link'
  }, {
    label: 'Invite by SMS',
    icon: 'i-lucide-message-square'
  }, {
    label: 'Invite by WhatsApp',
    icon: 'i-simple-icons-whatsapp'
  }]
}, {
  label: 'New team',
  icon: 'i-lucide-plus'
}]]
</script>

<template>
  <UDropdownMenu :items="items" :content="{ align: 'start' }" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [DropdownMenuOpenExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})

const items: DropdownMenuItem[] = [
  {
    label: 'Profile',
    icon: 'i-lucide-user'
  }, {
    label: 'Billing',
    icon: 'i-lucide-credit-card'
  }, {
    label: 'Settings',
    icon: 'i-lucide-cog'
  }
]
</script>

<template>
  <UDropdownMenu v-model:open="open" :items="items" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the DropdownMenu by pressing .

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [DropdownMenuCustomSlotExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items = [
  {
    label: 'Profile',
    icon: 'i-lucide-user',
    slot: 'profile' as const
  }, {
    label: 'Billing',
    icon: 'i-lucide-credit-card'
  }, {
    label: 'Settings',
    icon: 'i-lucide-cog'
  }
] satisfies DropdownMenuItem[]
</script>

<template>
  <UDropdownMenu :items="items" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />

    <template #profile-trailing>
      <UIcon name="i-lucide-badge-check" class="shrink-0 size-5 text-primary" />
    </template>
  </UDropdownMenu>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can also use the \`#item\`, \`#item-leading\`, \`#item-label\` and \`#item-trailing\` slots to customize all items.

### With switch in items

You can use the `slot` property with a `#{{ slot }}-trailing` slot to render a [Switch](https://ui.nuxt.com/docs/components/switch) inside an item.

```vue [DropdownMenuSwitchItemsExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const showBookmarks = ref(true)
const showHistory = ref(false)
const showDownloads = ref(false)

const items = computed(() => [{
  label: 'Show Bookmarks',
  icon: 'i-lucide-bookmark',
  slot: 'switch' as const,
  checked: showBookmarks.value,
  onSelect(e: Event) {
    e.preventDefault()
    showBookmarks.value = !showBookmarks.value
  }
}, {
  label: 'Show History',
  icon: 'i-lucide-clock',
  slot: 'switch' as const,
  checked: showHistory.value,
  onSelect(e: Event) {
    e.preventDefault()
    showHistory.value = !showHistory.value
  }
}, {
  label: 'Show Downloads',
  icon: 'i-lucide-download',
  slot: 'switch' as const,
  checked: showDownloads.value,
  onSelect(e: Event) {
    e.preventDefault()
    showDownloads.value = !showDownloads.value
  }
}] satisfies DropdownMenuItem[])
</script>

<template>
  <UDropdownMenu :items="items" :content="{ align: 'start' }" :ui="{ content: 'w-48' }">
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />

    <template #switch-trailing="{ item }">
      <USwitch :model-value="item.checked" tabindex="-1" />
    </template>
  </UDropdownMenu>
</template>
```

### With ignore filter `Soon`

When using the `filter` prop or the `filter` field on items with `children`, you can set the `ignore-filter` prop to `true` to disable the internal search and use your own search logic.

```vue [DropdownMenuIgnoreFilterExample.vue]
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'
import type { DropdownMenuItem } from '@nuxt/ui'

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'dropdown-menu-users-search',
  params: { q: searchTermDebounced },
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    })) as DropdownMenuItem[]
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UDropdownMenu
    v-model:search-term="searchTerm"
    :items="users || []"
    :filter="{
      icon: 'i-lucide-search',
      loading: status === 'pending'
    }"
    ignore-filter
    :content="{ align: 'start' }"
    :ui="{ content: 'w-48' }"
    @update:open="onOpen"
  >
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

\> \[!NOTE]
\> This example uses \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/#refdebounced) to debounce the API calls. The fetch is deferred with \`immediate: false\` so no request is made until the menu opens.

### With filter fields `Soon`

When using the `filter` prop or the `filter` field on items with `children`, you can set the `filter-fields` prop with an array of fields to filter on. Defaults to `[labelKey]`.

```vue [DropdownMenuFilterFieldsExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[] = [{
  label: 'Leanne Graham',
  description: 'Sincere@april.biz'
}, {
  label: 'Ervin Howell',
  description: 'Shanna@melissa.tv'
}, {
  label: 'Clementine Bauch',
  description: 'Nathan@yesenia.net'
}, {
  label: 'Patricia Lebsack',
  description: 'Julianne.OConner@kory.org'
}, {
  label: 'Chelsey Dietrich',
  description: 'Lucio_Hettinger@annie.me'
}]
</script>

<template>
  <UDropdownMenu
    :items="items"
    filter
    :filter-fields="['label', 'description']"
    :content="{ align: 'start' }"
    :ui="{ content: 'w-64' }"
  >
    <UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
  </UDropdownMenu>
</template>
```

### With trigger content width

You can expand the content to the full width of its button by adding the `w-(--reka-dropdown-menu-trigger-width)` class on the `ui.content` slot.

```vue [DropdownMenuContentWidthExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[][] = [
  [
    {
      label: 'View',
      icon: 'i-lucide-eye'
    },
    {
      label: 'Copy',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Edit',
      icon: 'i-lucide-pencil'
    }
  ],
  [
    {
      label: 'Delete',
      color: 'error',
      icon: 'i-lucide-trash'
    }
  ]
]
</script>

<template>
  <UDropdownMenu :items="items" :ui="{ content: 'w-(--reka-dropdown-menu-trigger-width)' }">
    <UButton
      label="Open"
      class="w-46"
      color="neutral"
      variant="outline"
      block
      trailing-icon="i-lucide-chevron-down"
    />
  </UDropdownMenu>
</template>
```

\> \[!TIP]
\> You can also change the content width globally in your \`app.config.ts\`:
\> \`\`\`text
\> export default defineAppConfig({
\> ui: {
\> dropdownMenu: {
\> slots: {
\> content: 'w-(--reka-dropdown-menu-trigger-width)'
\> }
\> }
\> }
\> })
\>
\> \`\`\`

### Extract shortcuts

Use the [extractShortcuts](https://ui.nuxt.com/docs/composables/extract-shortcuts) utility to automatically define shortcuts from menu items with a `kbds` property. It recursively extracts shortcuts and returns an object compatible with [defineShortcuts](https://ui.nuxt.com/docs/composables/define-shortcuts).

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[] = [{
  label: 'Invite users',
  icon: 'i-lucide-user-plus',
  children: [{
    label: 'Invite by email',
    icon: 'i-lucide-send-horizontal',
    kbds: ['meta', 'e'],
    onSelect() {
      console.log('Invite by email clicked')
    }
  }, {
    label: 'Invite by link',
    icon: 'i-lucide-link',
    kbds: ['meta', 'i'],
    onSelect() {
      console.log('Invite by link clicked')
    }
  }]
}, {
  label: 'New team',
  icon: 'i-lucide-plus',
  kbds: ['meta', 'n'],
  onSelect() {
    console.log('New team clicked')
  }
}]

defineShortcuts(extractShortcuts(items))
</script>
```

\> \[!NOTE]
\> In this example, , and would trigger the \`select\` function of the corresponding item.

## API

### Props

```ts
/**
 * Props for the DropdownMenu component
 */
interface DropdownMenuProps {
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  items?: T | undefined;
  /**
   * The icon displayed when an item is checked.
   */
  checkedIcon?: any;
  /**
   * The icon displayed when an item is loading.
   */
  loadingIcon?: any;
  /**
   * The icon displayed when the item is an external link.
   * Set to `false` to hide the external icon.
   * @default "true"
   */
  externalIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<DropdownMenuContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<MenuContentEmits>>) | undefined;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<DropdownMenuArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the menu in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the description from the item.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  /**
   * Whether to display a filter input or not.
   * Can be an object to pass additional props to the input.
   * `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
   * @default "false"
   */
  filter?: boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  /**
   * The fields to filter by.
   */
  filterFields?: string[] | undefined;
  /**
   * When `true`, items will not be filtered which is useful for custom filtering.
   * @default "false"
   */
  ignoreFilter?: boolean | undefined;
  disabled?: boolean | undefined;
  ui?: { content?: ClassNameValue; input?: ClassNameValue; empty?: ClassNameValue; viewport?: ClassNameValue; arrow?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } | undefined;
  /**
   * The open state of the dropdown menu when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the menu. Can be used as `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The modality of the dropdown menu.
   * 
   * When set to `true`, interaction with outside elements will be disabled and only menu content will be visible to screen readers.
   * @default "true"
   */
  modal?: boolean | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DropdownMenu component
 */
interface DropdownMenuSlots {
  default(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  empty(): any;
  content-top(): any;
  content-bottom(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the DropdownMenu component
 */
interface DropdownMenuEmits {
  update:open: (payload: [payload: boolean]) => void;
  update:searchTerm: (payload: [value: string]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    dropdownMenu: {
      slots: {
        content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
        input: 'border-b border-default',
        empty: 'text-center text-muted',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        arrow: 'fill-bg stroke-default',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0',
        itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
        itemTrailingKbdsSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:text-highlighted data-[state=open]:text-highlighted data-highlighted:before:bg-elevated/50 data-[state=open]:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:text-default group-data-[state=open]:text-default',
              'transition-colors'
            ]
          }
        },
        loading: {
          true: {
            itemLeadingIcon: 'animate-spin'
          }
        },
        size: {
          xs: {
            label: 'p-1 text-xs gap-1',
            item: 'p-1 text-xs gap-1',
            empty: 'p-2 text-xs',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemTrailingIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          sm: {
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            empty: 'p-2.5 text-xs',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemTrailingIcon: 'size-4',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'sm'
          },
          md: {
            label: 'p-1.5 text-sm gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            empty: 'p-2.5 text-sm',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemTrailingIcon: 'size-5',
            itemTrailingKbds: 'gap-0.5',
            itemTrailingKbdsSize: 'md'
          },
          lg: {
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-sm gap-2',
            empty: 'p-3 text-sm',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemTrailingIcon: 'size-5',
            itemTrailingKbds: 'gap-1',
            itemTrailingKbdsSize: 'md'
          },
          xl: {
            label: 'p-2 text-base gap-2',
            item: 'p-2 text-base gap-2',
            empty: 'p-3 text-base',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemTrailingIcon: 'size-6',
            itemTrailingKbds: 'gap-1',
            itemTrailingKbdsSize: 'lg'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          active: false,
          class: {
            item: 'text-primary data-highlighted:text-primary data-highlighted:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
            itemLeadingIcon: 'text-primary/75 group-data-highlighted:text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          active: false,
          class: {
            item: 'text-secondary data-highlighted:text-secondary data-highlighted:before:bg-secondary/10 data-[state=open]:before:bg-secondary/10',
            itemLeadingIcon: 'text-secondary/75 group-data-highlighted:text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          active: false,
          class: {
            item: 'text-success data-highlighted:text-success data-highlighted:before:bg-success/10 data-[state=open]:before:bg-success/10',
            itemLeadingIcon: 'text-success/75 group-data-highlighted:text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          active: false,
          class: {
            item: 'text-info data-highlighted:text-info data-highlighted:before:bg-info/10 data-[state=open]:before:bg-info/10',
            itemLeadingIcon: 'text-info/75 group-data-highlighted:text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          active: false,
          class: {
            item: 'text-warning data-highlighted:text-warning data-highlighted:before:bg-warning/10 data-[state=open]:before:bg-warning/10',
            itemLeadingIcon: 'text-warning/75 group-data-highlighted:text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          active: false,
          class: {
            item: 'text-error data-highlighted:text-error data-highlighted:before:bg-error/10 data-[state=open]:before:bg-error/10',
            itemLeadingIcon: 'text-error/75 group-data-highlighted:text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'primary',
          active: true,
          class: {
            item: 'text-primary before:bg-primary/10',
            itemLeadingIcon: 'text-primary'
          }
        },
        {
          color: 'secondary',
          active: true,
          class: {
            item: 'text-secondary before:bg-secondary/10',
            itemLeadingIcon: 'text-secondary'
          }
        },
        {
          color: 'success',
          active: true,
          class: {
            item: 'text-success before:bg-success/10',
            itemLeadingIcon: 'text-success'
          }
        },
        {
          color: 'info',
          active: true,
          class: {
            item: 'text-info before:bg-info/10',
            itemLeadingIcon: 'text-info'
          }
        },
        {
          color: 'warning',
          active: true,
          class: {
            item: 'text-warning before:bg-warning/10',
            itemLeadingIcon: 'text-warning'
          }
        },
        {
          color: 'error',
          active: true,
          class: {
            item: 'text-error before:bg-error/10',
            itemLeadingIcon: 'text-error'
          }
        }
      ],
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/DropdownMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/dropdown-menu.ts).


# Editor

## Usage

The Editor component provides a powerful rich text editing experience built on [TipTap](https://tiptap.dev/){rel="&#x22;nofollow&#x22;"}. It supports multiple content formats (JSON, HTML, Markdown), customizable toolbars, drag-and-drop block reordering, slash commands, mentions, emoji picker, and extensible architecture for adding custom functionality.

```vue [EditorExample.vue]
<script setup lang="ts">
import type { EditorCustomHandlers, EditorToolbarItem, EditorSuggestionMenuItem, EditorMentionMenuItem, EditorEmojiMenuItem, DropdownMenuItem } from '@nuxt/ui'
import type { Editor, JSONContent } from '@tiptap/vue-3'
import { upperFirst } from 'scule'
import { mapEditorItems } from '@nuxt/ui/utils/editor'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'
import { TextAlign } from '@tiptap/extension-text-align'
import { CodeBlockShiki } from 'tiptap-extension-code-block-shiki'
import { ImageUpload } from './EditorImageUploadExtension'
import { useEditorCompletion } from './EditorUseCompletion'
import EditorLinkPopover from './EditorLinkPopover.vue'

const editorRef = useTemplateRef('editorRef')

const value = ref(`# Building Modern Interfaces with Nuxt UI

Welcome to the **Nuxt UI Editor** — a powerful rich text editing experience built on [TipTap](https://tiptap.dev). This editor combines *flexibility* with ease of use, making content creation a breeze.

![Placeholder](/placeholder.jpeg)

## Rich Formatting Options

The editor supports all common text formatting including **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, and \`inline code\`. You can also combine them for **_bold and italic_** text.

### Interactive Features

Try out these powerful capabilities:

- **Bubble Menu** — Select any text to see formatting options appear
- **Slash Commands** — Type \`/\` for quick access to blocks and formatting
- **Mentions** — Use \`@\` to tag people or entities
- **Emoji Picker** — Type \`:\` followed by an emoji name like :smile:
- **Drag & Drop** — Hover over any block to see the drag handle

> **Pro tip:** You can use keyboard shortcuts like Cmd/Ctrl + B for bold, Cmd/Ctrl + I for italic, and more!

### Advanced Capabilities

1. **Custom Extensions** — Add your own TipTap extensions seamlessly
2. **Multiple Content Types** — Support for JSON, HTML, and Markdown
3. **Customizable Toolbars** — Fixed, bubble, and floating layouts
4. **Theme Integration** — Fully styled with Nuxt UI theme system

#### Code Blocks

Perfect for technical documentation:

\`\`\`vue
<template>
  <UEditor v-model="value" content-type="markdown" />
</template>
\`\`\`

---

Whether you're building a blog, documentation site, or content management system, the Nuxt UI Editor provides everything you need for a professional editing experience. Visit [ui.nuxt.com](https://ui.nuxt.com) to explore more components.`)

const { extension: completionExtension, handlers: aiHandlers, isLoading: aiLoading } = useEditorCompletion(editorRef)

const customHandlers = {
  imageUpload: {
    canExecute: (editor: Editor) => editor.can().insertContent({ type: 'imageUpload' }),
    execute: (editor: Editor) => editor.chain().focus().insertContent({ type: 'imageUpload' }),
    isActive: (editor: Editor) => editor.isActive('imageUpload'),
    isDisabled: undefined
  },
  ...aiHandlers
} satisfies EditorCustomHandlers

const fixedToolbarItems = [[{
  kind: 'undo',
  icon: 'i-lucide-undo',
  tooltip: { text: 'Undo' }
}, {
  kind: 'redo',
  icon: 'i-lucide-redo',
  tooltip: { text: 'Redo' }
}], [{
  icon: 'i-lucide-heading',
  tooltip: { text: 'Headings' },
  content: {
    align: 'start'
  },
  items: [{
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }, {
    kind: 'heading',
    level: 4,
    icon: 'i-lucide-heading-4',
    label: 'Heading 4'
  }]
}, {
  icon: 'i-lucide-list',
  tooltip: { text: 'Lists' },
  content: {
    align: 'start'
  },
  items: [{
    kind: 'bulletList',
    icon: 'i-lucide-list',
    label: 'Bullet List'
  }, {
    kind: 'orderedList',
    icon: 'i-lucide-list-ordered',
    label: 'Ordered List'
  }]
}, {
  kind: 'blockquote',
  icon: 'i-lucide-text-quote',
  tooltip: { text: 'Blockquote' }
}, {
  kind: 'codeBlock',
  icon: 'i-lucide-square-code',
  tooltip: { text: 'Code Block' }
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
  tooltip: { text: 'Bold' }
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
  tooltip: { text: 'Italic' }
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline',
  tooltip: { text: 'Underline' }
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough',
  tooltip: { text: 'Strikethrough' }
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
  tooltip: { text: 'Code' }
}], [{
  slot: 'link' as const,
  icon: 'i-lucide-link'
}, {
  kind: 'imageUpload',
  icon: 'i-lucide-image',
  tooltip: { text: 'Image' }
}], [{
  icon: 'i-lucide-align-justify',
  tooltip: { text: 'Text Align' },
  content: {
    align: 'end'
  },
  items: [{
    kind: 'textAlign',
    align: 'left',
    icon: 'i-lucide-align-left',
    label: 'Align Left'
  }, {
    kind: 'textAlign',
    align: 'center',
    icon: 'i-lucide-align-center',
    label: 'Align Center'
  }, {
    kind: 'textAlign',
    align: 'right',
    icon: 'i-lucide-align-right',
    label: 'Align Right'
  }, {
    kind: 'textAlign',
    align: 'justify',
    icon: 'i-lucide-align-justify',
    label: 'Align Justify'
  }]
}]] satisfies EditorToolbarItem<typeof customHandlers>[][]

const bubbleToolbarItems = computed(() => [[{
  icon: 'i-lucide-sparkles',
  label: 'Improve',
  activeColor: 'neutral',
  activeVariant: 'ghost',
  loading: aiLoading.value,
  content: {
    align: 'start'
  },
  items: [{
    kind: 'aiFix',
    icon: 'i-lucide-spell-check',
    label: 'Fix spelling & grammar'
  }, {
    kind: 'aiExtend',
    icon: 'i-lucide-unfold-vertical',
    label: 'Extend text'
  }, {
    kind: 'aiReduce',
    icon: 'i-lucide-fold-vertical',
    label: 'Reduce text'
  }, {
    kind: 'aiSimplify',
    icon: 'i-lucide-lightbulb',
    label: 'Simplify text'
  }, {
    kind: 'aiContinue',
    icon: 'i-lucide-text',
    label: 'Continue sentence'
  }, {
    kind: 'aiSummarize',
    icon: 'i-lucide-list',
    label: 'Summarize'
  }, {
    icon: 'i-lucide-languages',
    label: 'Translate',
    children: [{
      kind: 'aiTranslate',
      language: 'English',
      label: 'English'
    }, {
      kind: 'aiTranslate',
      language: 'French',
      label: 'French'
    }, {
      kind: 'aiTranslate',
      language: 'Spanish',
      label: 'Spanish'
    }, {
      kind: 'aiTranslate',
      language: 'German',
      label: 'German'
    }]
  }]
}], [{
  label: 'Turn into',
  trailingIcon: 'i-lucide-chevron-down',
  activeColor: 'neutral',
  activeVariant: 'ghost',
  tooltip: { text: 'Turn into' },
  content: {
    align: 'start'
  },
  ui: {
    label: 'text-xs'
  },
  items: [{
    type: 'label',
    label: 'Turn into'
  }, {
    kind: 'paragraph',
    label: 'Paragraph',
    icon: 'i-lucide-type'
  }, {
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }, {
    kind: 'heading',
    level: 4,
    icon: 'i-lucide-heading-4',
    label: 'Heading 4'
  }, {
    kind: 'bulletList',
    icon: 'i-lucide-list',
    label: 'Bullet List'
  }, {
    kind: 'orderedList',
    icon: 'i-lucide-list-ordered',
    label: 'Ordered List'
  }, {
    kind: 'blockquote',
    icon: 'i-lucide-text-quote',
    label: 'Blockquote'
  }, {
    kind: 'codeBlock',
    icon: 'i-lucide-square-code',
    label: 'Code Block'
  }]
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
  tooltip: { text: 'Bold' }
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
  tooltip: { text: 'Italic' }
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline',
  tooltip: { text: 'Underline' }
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough',
  tooltip: { text: 'Strikethrough' }
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
  tooltip: { text: 'Code' }
}], [{
  slot: 'link' as const,
  icon: 'i-lucide-link'
}, {
  kind: 'imageUpload',
  icon: 'i-lucide-image',
  tooltip: { text: 'Image' }
}], [{
  icon: 'i-lucide-align-justify',
  tooltip: { text: 'Text Align' },
  content: {
    align: 'end'
  },
  items: [{
    kind: 'textAlign',
    align: 'left',
    icon: 'i-lucide-align-left',
    label: 'Align Left'
  }, {
    kind: 'textAlign',
    align: 'center',
    icon: 'i-lucide-align-center',
    label: 'Align Center'
  }, {
    kind: 'textAlign',
    align: 'right',
    icon: 'i-lucide-align-right',
    label: 'Align Right'
  }, {
    kind: 'textAlign',
    align: 'justify',
    icon: 'i-lucide-align-justify',
    label: 'Align Justify'
  }]
}]] satisfies EditorToolbarItem<typeof customHandlers>[][])

const imageToolbarItems = (editor: Editor): EditorToolbarItem[][] => {
  const node = editor.state.doc.nodeAt(editor.state.selection.from)

  return [[{
    icon: 'i-lucide-download',
    to: node?.attrs?.src,
    download: true,
    tooltip: { text: 'Download' }
  }, {
    icon: 'i-lucide-refresh-cw',
    tooltip: { text: 'Replace' },
    onClick: () => {
      const { state } = editor
      const { selection } = state

      const pos = selection.from
      const node = state.doc.nodeAt(pos)

      if (node && node.type.name === 'image') {
        editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).insertContentAt(pos, { type: 'imageUpload' }).run()
      }
    }
  }], [{
    icon: 'i-lucide-trash',
    tooltip: { text: 'Delete' },
    onClick: () => {
      const { state } = editor
      const { selection } = state

      const pos = selection.from
      const node = state.doc.nodeAt(pos)

      if (node && node.type.name === 'image') {
        editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).run()
      }
    }
  }]]
}

const selectedNode = ref<{ node: JSONContent, pos: number }>()

const handleItems = (editor: Editor): DropdownMenuItem[][] => {
  if (!selectedNode.value?.node?.type) {
    return []
  }

  return mapEditorItems(editor, [[
    {
      type: 'label',
      label: upperFirst(selectedNode.value.node.type)
    },
    {
      label: 'Turn into',
      icon: 'i-lucide-repeat-2',
      children: [
        { kind: 'paragraph', label: 'Paragraph', icon: 'i-lucide-type' },
        { kind: 'heading', level: 1, label: 'Heading 1', icon: 'i-lucide-heading-1' },
        { kind: 'heading', level: 2, label: 'Heading 2', icon: 'i-lucide-heading-2' },
        { kind: 'heading', level: 3, label: 'Heading 3', icon: 'i-lucide-heading-3' },
        { kind: 'heading', level: 4, label: 'Heading 4', icon: 'i-lucide-heading-4' },
        { kind: 'bulletList', label: 'Bullet List', icon: 'i-lucide-list' },
        { kind: 'orderedList', label: 'Ordered List', icon: 'i-lucide-list-ordered' },
        { kind: 'blockquote', label: 'Blockquote', icon: 'i-lucide-text-quote' },
        { kind: 'codeBlock', label: 'Code Block', icon: 'i-lucide-square-code' }
      ]
    },
    {
      kind: 'clearFormatting',
      pos: selectedNode.value?.pos,
      label: 'Reset formatting',
      icon: 'i-lucide-rotate-ccw'
    }
  ], [
    {
      kind: 'duplicate',
      pos: selectedNode.value?.pos,
      label: 'Duplicate',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Copy to clipboard',
      icon: 'i-lucide-clipboard',
      onSelect: async () => {
        if (!selectedNode.value) return

        const pos = selectedNode.value.pos
        const node = editor.state.doc.nodeAt(pos)
        if (node) {
          await navigator.clipboard.writeText(node.textContent)
        }
      }
    }
  ], [
    {
      kind: 'moveUp',
      pos: selectedNode.value?.pos,
      label: 'Move up',
      icon: 'i-lucide-arrow-up'
    },
    {
      kind: 'moveDown',
      pos: selectedNode.value?.pos,
      label: 'Move down',
      icon: 'i-lucide-arrow-down'
    }
  ], [
    {
      kind: 'delete',
      pos: selectedNode.value?.pos,
      label: 'Delete',
      icon: 'i-lucide-trash'
    }
  ]], customHandlers) as DropdownMenuItem[][]
}

const suggestionItems = [[{
  type: 'label',
  label: 'AI'
}, {
  kind: 'aiContinue',
  label: 'Continue writing',
  icon: 'i-lucide-sparkles'
}], [{
  type: 'label',
  label: 'Style'
}, {
  kind: 'paragraph',
  label: 'Paragraph',
  icon: 'i-lucide-type'
}, {
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'heading',
  level: 3,
  label: 'Heading 3',
  icon: 'i-lucide-heading-3'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  label: 'Numbered List',
  icon: 'i-lucide-list-ordered'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}, {
  kind: 'codeBlock',
  label: 'Code Block',
  icon: 'i-lucide-square-code'
}], [{
  type: 'label',
  label: 'Insert'
}, {
  kind: 'mention',
  label: 'Mention',
  icon: 'i-lucide-at-sign'
}, {
  kind: 'emoji',
  label: 'Emoji',
  icon: 'i-lucide-smile-plus'
}, {
  kind: 'imageUpload',
  label: 'Image',
  icon: 'i-lucide-image'
}, {
  kind: 'horizontalRule',
  label: 'Horizontal Rule',
  icon: 'i-lucide-separator-horizontal'
}]] satisfies EditorSuggestionMenuItem<typeof customHandlers>[][]

const mentionItems: EditorMentionMenuItem[] = [{
  label: 'benjamincanac',
  avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4', loading: 'lazy' as const }
}, {
  label: 'HugoRCD',
  avatar: { src: 'https://avatars.githubusercontent.com/u/71938701?v=4', loading: 'lazy' as const }
}, {
  label: 'romhml',
  avatar: { src: 'https://avatars.githubusercontent.com/u/25613751?v=4', loading: 'lazy' as const }
}, {
  label: 'sandros94',
  avatar: { src: 'https://avatars.githubusercontent.com/u/13056429?v=4', loading: 'lazy' as const }
}, {
  label: 'hywax',
  avatar: { src: 'https://avatars.githubusercontent.com/u/149865959?v=4', loading: 'lazy' as const }
}, {
  label: 'J-Michalek',
  avatar: { src: 'https://avatars.githubusercontent.com/u/71264422?v=4', loading: 'lazy' as const }
}, {
  label: 'genu',
  avatar: { src: 'https://avatars.githubusercontent.com/u/928780?v=4', loading: 'lazy' as const }
}]

const emojiItems: EditorEmojiMenuItem[] = gitHubEmojis.filter(emoji => !emoji.name.startsWith('regional_indicator_'))
</script>

<template>
  <UEditor
    ref="editorRef"
    v-slot="{ editor, handlers }"
    v-model="value"
    content-type="markdown"
    :extensions="[
      Emoji,
      TextAlign.configure({ types: ['heading', 'paragraph'] }),
      ImageUpload,
      CodeBlockShiki.configure({
        defaultTheme: 'material-theme',
        themes: {
          light: 'material-theme-lighter',
          dark: 'material-theme-palenight'
        }
      }),
      completionExtension
    ]"
    :handlers="customHandlers"
    placeholder="Write, type '/' for commands..."
    :ui="{ base: 'p-8 sm:px-16 py-13.5' }"
    class="w-full"
  >
    <UEditorToolbar :editor="editor" :items="fixedToolbarItems" class="border-b border-muted sticky top-0 inset-x-0 px-8 sm:px-16 py-2 z-50 bg-default overflow-x-auto">
      <template #link>
        <EditorLinkPopover :editor="editor" auto-open />
      </template>
    </UEditorToolbar>

    <UEditorToolbar
      :editor="editor"
      :items="bubbleToolbarItems"
      layout="bubble"
      :should-show="({ editor, view, state }) => {
        if (editor.isActive('imageUpload') || editor.isActive('image')) {
          return false
        }
        const { selection } = state
        return view.hasFocus() && !selection.empty
      }"
    >
      <template #link>
        <EditorLinkPopover :editor="editor" />
      </template>
    </UEditorToolbar>

    <UEditorToolbar
      :editor="editor"
      :items="imageToolbarItems(editor)"
      layout="bubble"
      :should-show="({ editor, view }) => {
        return editor.isActive('image') && view.hasFocus()
      }"
    />

    <UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />

    <UEditorMentionMenu :editor="editor" :items="mentionItems" />

    <UEditorEmojiMenu :editor="editor" :items="emojiItems" />

    <UEditorDragHandle v-slot="{ ui, onClick }" :editor="editor" @node-change="selectedNode = $event">
      <UButton
        icon="i-lucide-plus"
        color="neutral"
        variant="ghost"
        size="sm"
        :class="ui.handle()"
        @click="(e) => {
          e.stopPropagation()

          const selected = onClick()
          handlers.suggestion?.execute(editor, { pos: selected?.pos }).run()
        }"
      />

      <UDropdownMenu
        v-slot="{ open }"
        :modal="false"
        :items="handleItems(editor)"
        :content="{ side: 'left' }"
        :ui="{ content: 'w-48', label: 'text-xs' }"
        @update:open="editor.chain().setMeta('lockDragHandle', $event).run()"
      >
        <UButton
          color="neutral"
          variant="ghost"
          active-variant="soft"
          size="sm"
          icon="i-lucide-grip-vertical"
          :active="open"
          :class="ui.handle()"
        />
      </UDropdownMenu>
    </UEditorDragHandle>
  </UEditor>
</template>

<style>
html.dark .tiptap .shiki,
html.dark .tiptap .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--ui-bg-muted) !important;
}
</style>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/docs/app/components/content/examples/editor/EditorExample.vue
\> This example demonstrates a production-ready Editor component. Check out the source code on GitHub.

\> \[!WARNING]
\> If you encounter prosemirror-related errors such as \`Adding different instances of a keyed plugin\` when using the Editor component or its extensions, you may need to add prosemirror packages to the \`vite.optimizeDeps.include\` list in your \`nuxt.config.ts\` file. This ensures Vite pre-bundles these dependencies to avoid loading multiple instances.
\> \`\`\`ts
\> export default defineNuxtConfig({
\> vite: {
\> optimizeDeps: {
\> include: \[
\> '@nuxt/ui > prosemirror-state',
\> '@nuxt/ui > prosemirror-transform',
\> '@nuxt/ui > prosemirror-model',
\> '@nuxt/ui > prosemirror-view',
\> '@nuxt/ui > prosemirror-gapcursor'
\> ]
\> }
\> }
\> })
\>
\> \`\`\`

### Content

Use the `v-model` directive to control the value of the Editor.

```vue
<template>
  <UEditor class="w-full min-h-21" />
</template>
```

### Content Type

The Editor automatically detects the content format based on `v-model` type: strings are treated as `html`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} and objects as `json`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

You can explicitly set the format using the `content-type` prop: `json`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, `html`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, or `markdown`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

```vue
<template>
  <UEditor model-value="<h1>Hello World</h1>
<p>This is a <strong>rich text</strong> editor.</p>
" content-type="html" class="w-full min-h-21" />
</template>
```

### Extensions

The Editor includes the following extensions by default:

- [**StarterKit**](https://ui.nuxt.com/#starter-kit) - Core editing features (bold, italic, headings, lists, etc.)
- [**Placeholder**](https://ui.nuxt.com/#placeholder) - Show placeholder text (when placeholder prop is provided)
- **Image** - Insert and display images
- **Mention** - Add @ mentions support
- **Markdown** - Parse and serialize markdown (when content type is markdown)

\> \[!NOTE]
\> Each built-in extension can be configured using its corresponding prop (\`starter-kit\`, \`placeholder\`, \`image\`, \`mention\`, \`markdown\`) to customize its behavior with TipTap options.

You can use the `extensions` prop to add additional TipTap extensions to enhance the Editor's capabilities:

```vue
<script setup lang="ts">
import { Emoji } from '@tiptap/extension-emoji'
import { TextAlign } from '@tiptap/extension-text-align'

const value = ref('<h1>Hello World</h1>\n')
</script>

<template>
  <UEditor
    v-model="value"
    :extensions="[
      Emoji,
      TextAlign.configure({
        types: ['heading', 'paragraph']
      })
    ]"
  />
</template>
```

\> \[!TIP]
\> See: #with-image-upload
\> Check out the image upload example for creating custom TipTap extensions.

### Placeholder

Use the `placeholder` prop to set a placeholder text that shows in empty paragraphs.

```vue
<template>
  <UEditor model-value="" placeholder="Start writing..." class="w-full min-h-7" />
</template>
```

\> \[!NOTE]
\> The \`placeholder\` prop accepts a string or an object with \[PlaceholderOptions]\(https\://tiptap.dev/docs/editor/extensions/functionality/placeholder) and an additional \`mode\` property:\`everyLine\`: Display placeholder on every empty line when focused (default).\`firstLine\`: Display placeholder only on the first line when the editor is empty.
\> \`\`\`vue
\> \<template>
\> \<UEditor \:placeholder="{ placeholder: 'Start writing...', mode: 'firstLine' }" />
\> \</template>
\>
\> \`\`\`

\> \[!TIP]
\> By default, placeholders only appear on top-level empty nodes. To show placeholders in nested elements like list items, set \`includeChildren\` to \`true\`:
\> \`\`\`vue
\> \<template>
\> \<UEditor \:placeholder="{ placeholder: 'Start writing...', includeChildren: true }" />
\> \</template>
\>
\> \`\`\`

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/functionality/placeholder
\> Learn more about Placeholder extension in the TipTap documentation.

### Starter Kit

Use the `starter-kit` prop to configure the built-in TipTap StarterKit extension which includes common editor features like bold, italic, headings, lists, blockquotes, code blocks, and more.

```vue
<script setup lang="ts">
const value = ref('<h1>Hello World</h1>\n')
</script>

<template>
  <UEditor
    v-model="value"
    :starter-kit="{
      blockquote: false,
      headings: {
        levels: [1, 2, 3, 4]
      },
      dropcursor: {
        color: 'var(--ui-primary)',
        width: 2
      },
      link: {
        openOnClick: false
      }
    }"
  />
</template>
```

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/functionality/starterkit
\> Learn more about StarterKit extension in the TipTap documentation.

### Handlers

Handlers wrap TipTap's built-in commands to provide a unified interface for editor actions. When you add a `kind` property to a [EditorToolbar](https://ui.nuxt.com/docs/components/editor-toolbar) or [EditorSuggestionMenu](https://ui.nuxt.com/docs/components/editor-suggestion-menu) item, the corresponding handler executes the TipTap command and manages its state (active, disabled, etc.).

#### Default handlers

The Editor component provides these default handlers, which you can reference in toolbar or suggestion menu items using the `kind` property:

| Handler                                                                                                                               | Description                                               | Usage                             |
| ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------- |
| `mark`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}            | Toggle text marks (bold, italic, strike, code, underline) | Requires `mark` property in item  |
| `textAlign`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}       | Set text alignment (left, center, right, justify)         | Requires `align` property in item |
| `heading`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}         | Toggle heading levels (1-6)                               | Requires `level` property in item |
| `link`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}            | Add, edit, or remove links                                | Prompts for URL if not provided   |
| `image`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}           | Insert images                                             | Prompts for URL if not provided   |
| `blockquote`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}      | Toggle blockquotes                                        |                                   |
| `bulletList`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}      | Toggle bullet lists                                       | Handles list conversions          |
| `orderedList`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}     | Toggle ordered lists                                      | Handles list conversions          |
| `taskList`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}        | Toggle task lists                                         | Handles list conversions          |
| `codeBlock`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}       | Toggle code blocks                                        |                                   |
| `horizontalRule`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}  | Insert horizontal rules                                   |                                   |
| `paragraph`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}       | Set paragraph format                                      |                                   |
| `undo`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}            | Undo last change                                          |                                   |
| `redo`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}            | Redo last undone change                                   |                                   |
| `clearFormatting`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | Remove all formatting                                     | Works with selection or position  |
| `duplicate`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}       | Duplicate a node                                          | Requires `pos` property in item   |
| `delete`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}          | Delete a node                                             | Requires `pos` property in item   |
| `moveUp`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}          | Move a node up                                            | Requires `pos` property in item   |
| `moveDown`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}        | Move a node down                                          | Requires `pos` property in item   |
| `suggestion`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}      | Trigger suggestion menu                                   | Inserts `/` character             |
| `mention`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}         | Trigger mention menu                                      | Inserts `@` character             |
| `emoji`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}           | Trigger emoji picker                                      | Inserts `:` character             |

\> \[!WARNING]
\> The \`taskList\` and \`textAlign\` handlers only work when their respective extensions are installed, as they are not included in the Editor by default.

Here's how to use default handlers in toolbar or suggestion menu items:

```vue
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref('<h1>Hello World</h1>\n')

const items: EditorToolbarItem[] = [
  { kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
  { kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
  { kind: 'heading', level: 1, icon: 'i-lucide-heading-1' },
  { kind: 'heading', level: 2, icon: 'i-lucide-heading-2' },
  { kind: 'textAlign', align: 'left', icon: 'i-lucide-align-left' },
  { kind: 'textAlign', align: 'center', icon: 'i-lucide-align-center' },
  { kind: 'bulletList', icon: 'i-lucide-list' },
  { kind: 'orderedList', icon: 'i-lucide-list-ordered' },
  { kind: 'blockquote', icon: 'i-lucide-quote' },
  { kind: 'link', icon: 'i-lucide-link' }
]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value">
    <UEditorToolbar :editor="editor" :items="items" />
  </UEditor>
</template>
```

#### Custom handlers

Use the `handlers` prop to extend or override the default handlers. Custom handlers are merged with the default handlers, allowing you to add new actions or modify existing behavior.

Each handler implements the `EditorHandler`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} interface:

```ts
interface EditorHandler {
  /* Checks if the command can be executed in the current editor state */
  canExecute: (editor: Editor, item?: any) => boolean
  /* Executes the command and returns a Tiptap chain */
  execute: (editor: Editor, item?: any) => any
  /* Determines if the item should appear active (used for toggle states) */
  isActive: (editor: Editor, item?: any) => boolean
  /* Optional additional check to disable the item (combined with `canExecute`) */
  isDisabled?: (editor: Editor, item?: any) => boolean
}
```

Here's an example of creating custom handlers:

```vue
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'
import type { EditorCustomHandlers, EditorToolbarItem } from '@nuxt/ui'

const value = ref('<h1>Hello World</h1>\n')

const customHandlers = {
  highlight: {
    canExecute: (editor: Editor) => editor.can().toggleHighlight(),
    execute: (editor: Editor) => editor.chain().focus().toggleHighlight(),
    isActive: (editor: Editor) => editor.isActive('highlight'),
    isDisabled: (editor: Editor) => !editor.isEditable
  }
} satisfies EditorCustomHandlers

const items = [
  // Built-in handler
  { kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
  // Custom handler
  { kind: 'highlight', icon: 'i-lucide-highlighter' }
] satisfies EditorToolbarItem<typeof customHandlers>[]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" :handlers="customHandlers">
    <UEditorToolbar :editor="editor" :items="items" />
  </UEditor>
</template>
```

\> \[!TIP]
\> See: #with-image-upload
\> Check out the image upload example for a complete implementation with custom handlers.

## Examples

\> \[!NOTE]
\> See: https\://github.com/nuxt-ui-templates/editor
\> Check out the source code of our Editor template on GitHub for a real-life example.

### With toolbar

You can use the [EditorToolbar](https://ui.nuxt.com/docs/components/editor-toolbar) component to add a `fixed`, `bubble`, or `floating` toolbar to the Editor with common formatting actions.

```vue [EditorToolbarExample.vue]
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: EditorToolbarItem[][] = [[{
  icon: 'i-lucide-heading',
  tooltip: { text: 'Headings' },
  content: {
    align: 'start'
  },
  items: [{
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }, {
    kind: 'heading',
    level: 4,
    icon: 'i-lucide-heading-4',
    label: 'Heading 4'
  }]
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
  tooltip: { text: 'Bold' }
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
  tooltip: { text: 'Italic' }
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline',
  tooltip: { text: 'Underline' }
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough',
  tooltip: { text: 'Strikethrough' }
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
  tooltip: { text: 'Code' }
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-21"
  >
    <UEditorToolbar :editor="editor" :items="items" layout="bubble" />
  </UEditor>
</template>
```

### With drag handle

You can use the [EditorDragHandle](https://ui.nuxt.com/docs/components/editor-drag-handle) component to add a draggable handle for reordering blocks.

```vue [EditorDragHandleExample.vue]
<script setup lang="ts">
const value = ref(`# Drag Handle

Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-21"
  >
    <UEditorDragHandle :editor="editor" />
  </UEditor>
</template>
```

### With suggestion menu

You can use the [EditorSuggestionMenu](https://ui.nuxt.com/docs/components/editor-suggestion-menu) component to add slash commands for quick formatting and insertions.

```vue [EditorSuggestionMenuExample.vue]
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'

const value = ref(`# Suggestion Menu

Type / to open the suggestion menu and browse available formatting commands.`)

const items: EditorSuggestionMenuItem[][] = [[{
  type: 'label',
  label: 'Text'
}, {
  kind: 'paragraph',
  label: 'Paragraph',
  icon: 'i-lucide-type'
}, {
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'heading',
  level: 3,
  label: 'Heading 3',
  icon: 'i-lucide-heading-3'
}], [{
  type: 'label',
  label: 'Lists'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  label: 'Numbered List',
  icon: 'i-lucide-list-ordered'
}], [{
  type: 'label',
  label: 'Insert'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}, {
  kind: 'codeBlock',
  label: 'Code Block',
  icon: 'i-lucide-square-code'
}, {
  kind: 'horizontalRule',
  label: 'Divider',
  icon: 'i-lucide-separator-horizontal'
}]]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type / for commands..."
    class="w-full min-h-21"
  >
    <UEditorSuggestionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

### With mention menu

You can use the [EditorMentionMenu](https://ui.nuxt.com/docs/components/editor-mention-menu) component to add @ mentions for tagging users or entities.

```vue [EditorMentionMenuExample.vue]
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'

const value = ref(`# Mention Menu

Type @ to mention someone and select from the list of available users.`)

const items: EditorMentionMenuItem[] = [{
  label: 'benjamincanac',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/739984?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'atinux',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/904724?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'danielroe',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/28706372?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'pi0',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/5158436?v=4',
    loading: 'lazy' as const
  }
}]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention someone..."
    class="w-full min-h-21"
  >
    <UEditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

### With emoji menu

You can use the [EditorEmojiMenu](https://ui.nuxt.com/docs/components/editor-emoji-menu) component to add emoji picker support.

```vue [EditorEmojiMenuExample.vue]
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'

const value = ref(`# Emoji Menu

Type : to insert emojis and select from the list of available emojis.`)

const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(emoji => !emoji.name.startsWith('regional_indicator_'))

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[Emoji]"
    content-type="markdown"
    placeholder="Type : to add emojis..."
    class="w-full min-h-21"
  >
    <UEditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

### With image upload

This example demonstrates how to create an image upload feature using the `extensions` prop to register a custom TipTap node and the `handlers` prop to define how the toolbar button triggers the upload flow.

1. Create a Vue component that uses the [FileUpload](https://ui.nuxt.com/docs/components/file-upload) component:

```vue [EditorImageUploadNode.vue]
<script setup lang="ts">
import type { NodeViewProps } from '@tiptap/vue-3'
import { NodeViewWrapper } from '@tiptap/vue-3'

const props = defineProps<NodeViewProps>()

const file = ref<File | null>(null)
const loading = ref(false)

watch(file, async (newFile) => {
  if (!newFile) return

  loading.value = true

  const reader = new FileReader()
  reader.onload = async (e) => {
    const dataUrl = e.target?.result as string
    if (!dataUrl) {
      loading.value = false
      return
    }

    // Simulate upload delay
    await new Promise(resolve => setTimeout(resolve, 1000))

    const pos = props.getPos()
    if (typeof pos !== 'number') {
      loading.value = false
      return
    }

    props.editor
      .chain()
      .focus()
      .deleteRange({ from: pos, to: pos + 1 })
      .setImage({ src: dataUrl })
      .run()

    loading.value = false
  }
  reader.readAsDataURL(newFile)
})
</script>

<template>
  <NodeViewWrapper>
    <UFileUpload
      v-model="file"
      accept="image/*"
      label="Upload an image"
      description="SVG, PNG, JPG or GIF (max. 2MB)"
      :preview="false"
      class="min-h-48"
    >
      <template #leading>
        <UAvatar
          :icon="loading ? 'i-lucide-loader-circle' : 'i-lucide-image'"
          size="xl"
          :ui="{ icon: [loading && 'animate-spin'] }"
        />
      </template>
    </UFileUpload>
  </NodeViewWrapper>
</template>
```

2. Create a custom TipTap extension to register the node:

```vue [EditorImageUploadExtension.vue]
import { Node, mergeAttributes } from '@tiptap/core'
import type { CommandProps, NodeViewRenderer } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import ImageUploadNodeComponent from './EditorImageUploadNode.vue'

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    imageUpload: {
      insertImageUpload: () => ReturnType
    }
  }
}

export const ImageUpload = Node.create({
  name: 'imageUpload',
  group: 'block',
  atom: true,
  draggable: true,
  addAttributes() {
    return {}
  },
  parseHTML() {
    return [{
      tag: 'div[data-type="image-upload"]'
    }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'image-upload' })]
  },
  addNodeView(): NodeViewRenderer {
    return VueNodeViewRenderer(ImageUploadNodeComponent)
  },
  addCommands() {
    return {
      insertImageUpload: () => ({ commands }: CommandProps) => {
        return commands.insertContent({ type: this.name })
      }
    }
  }
})

export default ImageUpload
```

3. Use the custom extension in the Editor:

```vue [EditorImageUploadExample.vue]
<script setup lang="ts">
import type { EditorCustomHandlers, EditorToolbarItem } from '@nuxt/ui'
import type { Editor } from '@tiptap/vue-3'
import { ImageUpload } from './EditorImageUploadExtension'

const value = ref(`# Image Upload

This editor demonstrates how to create a custom TipTap extension with handlers. Click the image button in the toolbar to upload a file — it will show a custom [FileUpload](/docs/components/file-upload) interface before inserting the image.

Try uploading an image below:

`)

const customHandlers = {
  imageUpload: {
    canExecute: (editor: Editor) => editor.can().insertContent({ type: 'imageUpload' }),
    execute: (editor: Editor) => editor.chain().focus().insertContent({ type: 'imageUpload' }),
    isActive: (editor: Editor) => editor.isActive('imageUpload'),
    isDisabled: undefined
  }
} satisfies EditorCustomHandlers

const items = [[{
  kind: 'imageUpload',
  icon: 'i-lucide-image',
  label: 'Add image',
  variant: 'soft'
}], [{
  icon: 'i-lucide-heading',
  content: {
    align: 'start'
  },
  items: [{
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }, {
    kind: 'heading',
    level: 4,
    icon: 'i-lucide-heading-4',
    label: 'Heading 4'
  }]
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline'
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough'
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code'
}]] satisfies EditorToolbarItem<typeof customHandlers>[][]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[ImageUpload]"
    :handlers="customHandlers"
    content-type="markdown"
    :ui="{ base: 'p-8 sm:px-16' }"
    class="w-full min-h-74"
  >
    <UEditorToolbar :editor="editor" :items="items" class="border-b border-muted py-2 px-8 sm:px-16 overflow-x-auto" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/custom-extensions
\> Learn more about creating custom extensions in the TipTap documentation.

### With AI completion

This example demonstrates how to add AI-powered features to the Editor using the [Vercel AI SDK](https://ai-sdk.dev/){rel="&#x22;nofollow&#x22;"}, specifically the [`useCompletion`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-completion){rel="&#x22;nofollow&#x22;"} composable for streaming text completions, combined with the [Vercel AI Gateway](https://vercel.com/ai-gateway){rel="&#x22;nofollow&#x22;"} to access AI models through a centralized endpoint. It includes ghost text autocompletion and text transformation actions (fix grammar, extend, reduce, simplify, translate, etc.).

\> \[!NOTE]
\> You need to install these dependencies first to use this example:
\> \`\`\`bash
\> pnpm add ai @ai-sdk/gateway @ai-sdk/vue
\>
\> \`\`\`
\>
\> \`\`\`bash
\> yarn add ai @ai-sdk/gateway @ai-sdk/vue
\>
\> \`\`\`
\>
\> \`\`\`bash
\> npm install ai @ai-sdk/gateway @ai-sdk/vue
\>
\> \`\`\`
\>
\> \`\`\`bash
\> bun add ai @ai-sdk/gateway @ai-sdk/vue
\>
\> \`\`\`

1. Create a custom TipTap extension that handles inline ghost text suggestions:

```vue [EditorCompletionExtension.vue]
import { Extension } from '@tiptap/core'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import type { Editor } from '@tiptap/vue-3'
import { useDebounceFn } from '@vueuse/core'

export interface CompletionOptions {
  /**
   * Debounce delay in ms before triggering completion
   * @defaultValue 250
   */
  debounce?: number
  /**
   * Whether to automatically trigger completion while typing
   * @defaultValue false
   */
  autoTrigger?: boolean
  /**
   * Characters that should prevent completion from triggering
   * @defaultValue ['/', ':', '@']
   */
  triggerCharacters?: string[]
  /**
   * Called when completion should be triggered, receives the editor instance
   */
  onTrigger?: (editor: Editor) => void
  /**
   * Called when suggestion is accepted
   */
  onAccept?: () => void
  /**
   * Called when suggestion is dismissed
   */
  onDismiss?: () => void
}

export interface CompletionStorage {
  suggestion: string
  position: number | undefined
  visible: boolean
  debouncedTrigger: ((editor: Editor) => void) | null
  setSuggestion: (text: string) => void
  clearSuggestion: () => void
}

export const completionPluginKey = new PluginKey('completion')

export const Completion = Extension.create<CompletionOptions, CompletionStorage>({
  name: 'completion',

  addOptions() {
    return {
      debounce: 250,
      autoTrigger: false,
      triggerCharacters: ['/', ':', '@'],
      onTrigger: undefined,
      onAccept: undefined,
      onDismiss: undefined
    }
  },

  addStorage() {
    return {
      suggestion: '',
      position: undefined as number | undefined,
      visible: false,
      debouncedTrigger: null as ((editor: Editor) => void) | null,
      setSuggestion(text: string) {
        this.suggestion = text
      },
      clearSuggestion() {
        this.suggestion = ''
        this.position = undefined
        this.visible = false
      }
    }
  },

  addProseMirrorPlugins() {
    const storage = this.storage

    return [
      new Plugin({
        key: completionPluginKey,
        props: {
          decorations(state) {
            if (!storage.visible || !storage.suggestion || storage.position === undefined) {
              return DecorationSet.empty
            }

            const widget = Decoration.widget(storage.position, () => {
              const span = document.createElement('span')
              span.className = 'completion-suggestion'
              span.textContent = storage.suggestion
              span.style.cssText = 'color: var(--ui-text-muted); opacity: 0.6; pointer-events: none;'
              return span
            }, { side: 1 })

            return DecorationSet.create(state.doc, [widget])
          }
        }
      })
    ]
  },

  addKeyboardShortcuts() {
    return {
      'Mod-j': ({ editor }) => {
        // Clear any existing suggestion first to avoid flickering
        if (this.storage.visible) {
          this.storage.clearSuggestion()
          this.options.onDismiss?.()
        }
        // Manually trigger completion
        this.storage.debouncedTrigger?.(editor as Editor)
        return true
      },
      'Tab': ({ editor }) => {
        if (!this.storage.visible || !this.storage.suggestion || this.storage.position === undefined) {
          return false
        }

        // Store values before clearing
        const suggestion = this.storage.suggestion
        const position = this.storage.position

        // Clear suggestion first
        this.storage.clearSuggestion()

        // Force decoration update
        editor.view.dispatch(editor.state.tr.setMeta('completionUpdate', true))

        // Insert the suggestion text
        editor.chain().focus().insertContentAt(position, suggestion).run()

        this.options.onAccept?.()
        return true
      },
      'Escape': ({ editor }) => {
        if (this.storage.visible) {
          this.storage.clearSuggestion()
          // Force decoration update
          editor.view.dispatch(editor.state.tr.setMeta('completionUpdate', true))
          this.options.onDismiss?.()
          return true
        }
        return false
      }
    }
  },

  onUpdate({ editor }) {
    // Clear suggestion on any edit
    if (this.storage.visible) {
      this.storage.clearSuggestion()
      // Force decoration update
      editor.view.dispatch(editor.state.tr.setMeta('completionUpdate', true))
      this.options.onDismiss?.()
    }

    // Debounced trigger check (only if autoTrigger is enabled)
    if (this.options.autoTrigger) {
      this.storage.debouncedTrigger?.(editor as Editor)
    }
  },

  onSelectionUpdate({ editor }) {
    if (this.storage.visible) {
      this.storage.clearSuggestion()
      // Force decoration update
      editor.view.dispatch(editor.state.tr.setMeta('completionUpdate', true))
      this.options.onDismiss?.()
    }
  },

  onCreate() {
    const storage = this.storage
    const options = this.options

    // Create debounced trigger function for this instance
    this.storage.debouncedTrigger = useDebounceFn((editor: Editor) => {
      if (!options.onTrigger) return

      const { state } = editor
      const { selection } = state
      const { $from } = selection

      // Only suggest at end of block with content
      const isAtEndOfBlock = $from.parentOffset === $from.parent.content.size
      const hasContent = $from.parent.textContent.trim().length > 0
      const textContent = $from.parent.textContent

      // Don't trigger if sentence is complete (ends with punctuation)
      const endsWithPunctuation = /[.!?]\s*$/.test(textContent)

      // Don't trigger if text ends with trigger characters
      const triggerChars = options.triggerCharacters || []
      const endsWithTrigger = triggerChars.some(char => textContent.endsWith(char))

      if (!isAtEndOfBlock || !hasContent || endsWithPunctuation || endsWithTrigger) {
        return
      }

      // Set position and mark as visible
      storage.position = selection.from
      storage.visible = true

      // Pass editor to let the handler extract content (e.g., as markdown)
      options.onTrigger(editor)
    }, options.debounce || 250)
  },

  onDestroy() {
    this.storage.debouncedTrigger = null
  }
})

export default Completion
```

2. Create a composable that manages AI completion state and handlers:

```vue [EditorUseCompletion.vue]
import { useCompletion } from '@ai-sdk/vue'
import type { Editor } from '@tiptap/vue-3'
import { Completion } from './EditorCompletionExtension'
import type { CompletionStorage } from './EditorCompletionExtension'

type CompletionMode = 'continue' | 'fix' | 'extend' | 'reduce' | 'simplify' | 'summarize' | 'translate'

export interface UseEditorCompletionOptions {
  api?: string
}

export function useEditorCompletion(editorRef: Ref<{ editor: Editor | undefined } | null | undefined>, options: UseEditorCompletionOptions = {}) {
  // State for direct insertion/transform mode
  const insertState = ref<{
    pos: number
    deleteRange?: { from: number, to: number }
  }>()
  const mode = ref<CompletionMode>('continue')
  const language = ref<string>()

  // Helper to get completion storage
  function getCompletionStorage() {
    const storage = editorRef.value?.editor?.storage as Record<string, CompletionStorage> | undefined
    return storage?.completion
  }

  const { completion, complete, isLoading, stop, setCompletion } = useCompletion({
    api: options.api || '/api/completion',
    streamProtocol: 'text',
    body: computed(() => ({
      mode: mode.value,
      language: language.value
    })),
    onFinish: (_prompt, completionText) => {
      // For inline suggestion mode, don't clear - let user accept with Tab
      const storage = getCompletionStorage()
      if (mode.value === 'continue' && storage?.visible) {
        return
      }

      // For transform modes, insert the full completion with markdown parsing
      const transformModes = ['fix', 'extend', 'reduce', 'simplify', 'summarize', 'translate']
      if (transformModes.includes(mode.value) && insertState.value && completionText) {
        const editor = editorRef.value?.editor
        if (editor) {
          // Delete the original selection if not already done
          if (insertState.value.deleteRange) {
            editor.chain()
              .focus()
              .deleteRange(insertState.value.deleteRange)
              .run()
          }

          // Insert with markdown parsing
          editor.chain()
            .focus()
            .insertContentAt(insertState.value.pos, completionText, { contentType: 'markdown' })
            .run()
        }
      }

      insertState.value = undefined
    },
    onError: (error) => {
      console.error('AI completion error:', error)
      insertState.value = undefined
      getCompletionStorage()?.clearSuggestion()
    }
  })

  // Watch completion for inline suggestion updates
  watch(completion, (newCompletion, oldCompletion) => {
    const editor = editorRef.value?.editor
    if (!editor || !newCompletion) return

    const storage = getCompletionStorage()
    if (storage?.visible) {
      // Update inline suggestion
      // Add space prefix if needed (so preview matches what will be inserted)
      let suggestionText = newCompletion
      if (storage.position !== undefined) {
        const textBefore = editor.state.doc.textBetween(Math.max(0, storage.position - 1), storage.position)
        if (textBefore && !/\s/.test(textBefore) && !suggestionText.startsWith(' ')) {
          suggestionText = ' ' + suggestionText
        }
      }
      storage.setSuggestion(suggestionText)
      editor.view.dispatch(editor.state.tr.setMeta('completionUpdate', true))
    } else if (insertState.value) {
      // Direct insertion/transform mode (from toolbar actions)

      // Transform modes use markdown insertion - wait for full completion
      const transformModes = ['fix', 'extend', 'reduce', 'simplify', 'summarize', 'translate']
      if (transformModes.includes(mode.value)) {
        // Don't stream - will be handled in onFinish
        return
      }

      // If this is the first chunk and we have a selection to replace, delete it first
      if (insertState.value.deleteRange && !oldCompletion) {
        editor.chain()
          .focus()
          .deleteRange(insertState.value.deleteRange)
          .run()
        insertState.value.deleteRange = undefined
      }

      let delta = newCompletion.slice(oldCompletion?.length || 0)
      if (delta) {
        // For single-paragraph transforms, replace all line breaks with spaces
        if (['fix', 'simplify', 'translate'].includes(mode.value)) {
          delta = delta.replace(/[\r\n]+/g, ' ').replace(/\s{2,}/g, ' ')
        }

        // For "continue" mode, add a space before if needed (first chunk only)
        if (mode.value === 'continue' && !oldCompletion) {
          const textBefore = editor.state.doc.textBetween(Math.max(0, insertState.value.pos - 1), insertState.value.pos)
          if (textBefore && !/\s/.test(textBefore)) {
            delta = ' ' + delta
          }
        }

        editor.chain().focus().command(({ tr }) => {
          tr.insertText(delta, insertState.value!.pos)
          return true
        }).run()
        insertState.value.pos += delta.length
      }
    }
  })

  function triggerTransform(editor: Editor, transformMode: Exclude<CompletionMode, 'continue'>, lang?: string) {
    if (isLoading.value) return

    getCompletionStorage()?.clearSuggestion()

    const { state } = editor
    const { selection } = state

    if (selection.empty) return

    mode.value = transformMode
    language.value = lang
    const selectedText = state.doc.textBetween(selection.from, selection.to)

    // Replace the selected text with the transformed version
    insertState.value = { pos: selection.from, deleteRange: { from: selection.from, to: selection.to } }

    complete(selectedText)
  }

  function getMarkdownBefore(editor: Editor, pos: number): string {
    const { state } = editor
    const serializer = (editor.storage.markdown as { serializer?: { serialize: (content: unknown) => string } })?.serializer
    if (serializer) {
      const slice = state.doc.slice(0, pos)
      return serializer.serialize(slice.content)
    }
    // Fallback to plain text
    return state.doc.textBetween(0, pos, '\n')
  }

  function triggerContinue(editor: Editor) {
    if (isLoading.value) return

    mode.value = 'continue'
    getCompletionStorage()?.clearSuggestion()
    const { state } = editor
    const { selection } = state

    if (selection.empty) {
      // No selection: continue from cursor position
      const textBefore = getMarkdownBefore(editor, selection.from)
      insertState.value = { pos: selection.from }
      complete(textBefore)
    } else {
      // Text selected: append completion after the selection
      const textBefore = getMarkdownBefore(editor, selection.to)
      insertState.value = { pos: selection.to }
      complete(textBefore)
    }
  }

  // Configure Completion extension
  const extension = Completion.configure({
    onTrigger: (editor) => {
      if (isLoading.value) return
      mode.value = 'continue'
      const textBefore = getMarkdownBefore(editor, editor.state.selection.from)
      complete(textBefore)
    },
    onAccept: () => {
      setCompletion('')
    },
    onDismiss: () => {
      stop()
      setCompletion('')
    }
  })

  // Create handlers for toolbar
  const handlers = {
    aiContinue: {
      canExecute: () => !isLoading.value,
      execute: (editor: Editor) => {
        triggerContinue(editor)
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'continue'),
      isDisabled: () => !!isLoading.value
    },
    aiFix: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor) => {
        triggerTransform(editor, 'fix')
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'fix'),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    },
    aiExtend: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor) => {
        triggerTransform(editor, 'extend')
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'extend'),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    },
    aiReduce: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor) => {
        triggerTransform(editor, 'reduce')
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'reduce'),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    },
    aiSimplify: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor) => {
        triggerTransform(editor, 'simplify')
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'simplify'),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    },
    aiSummarize: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor) => {
        triggerTransform(editor, 'summarize')
        return editor.chain()
      },
      isActive: () => !!(isLoading.value && mode.value === 'summarize'),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    },
    aiTranslate: {
      canExecute: (editor: Editor) => !editor.state.selection.empty && !isLoading.value,
      execute: (editor: Editor, cmd: { language?: string } | undefined) => {
        triggerTransform(editor, 'translate', cmd?.language)
        return editor.chain()
      },
      isActive: (_editor: Editor, cmd: { language?: string } | undefined) => !!(isLoading.value && mode.value === 'translate' && language.value === cmd?.language),
      isDisabled: (editor: Editor) => editor.state.selection.empty || !!isLoading.value
    }
  }

  return {
    extension,
    handlers,
    isLoading,
    mode
  }
}
```

3. Create a server API endpoint to handle completion requests using [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext){rel="&#x22;nofollow&#x22;"}:

\`\`\`ts
import { streamText } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { prompt, mode, language } = await readBody(event)
if (!prompt) {
throw createError({ statusCode: 400, message: 'Prompt is required' })
}
let system: string
let maxOutputTokens: number
const preserveMarkdown = 'IMPORTANT: Preserve all markdown formatting (bold, italic, links, etc.) exactly as in the original.'
switch (mode) {
case 'fix':
system = \`You are a writing assistant. Fix all spelling and grammar errors in the given text. ${preserveMarkdown} Only output the corrected text, nothing else.\`
maxOutputTokens = 500
break
case 'extend':
system = \`You are a writing assistant. Extend the given text with more details, examples, and explanations while maintaining the same style. ${preserveMarkdown} Only output the extended text, nothing else.\`
maxOutputTokens = 500
break
case 'reduce':
system = \`You are a writing assistant. Make the given text more concise by removing unnecessary words while keeping the meaning. ${preserveMarkdown} Only output the reduced text, nothing else.\`
maxOutputTokens = 300
break
case 'simplify':
system = \`You are a writing assistant. Simplify the given text to make it easier to understand, using simpler words and shorter sentences. ${preserveMarkdown} Only output the simplified text, nothing else.\`
maxOutputTokens = 400
break
case 'summarize':
system = 'You are a writing assistant. Summarize the given text concisely while keeping the key points. Only output the summary, nothing else.'
maxOutputTokens = 200
break
case 'translate':
system = \`You are a writing assistant. Translate the given text to ${language || 'English'}. ${preserveMarkdown} Only output the translated text, nothing else.\`
maxOutputTokens = 500
break
case 'continue':
default:
system = \`You are a writing assistant providing inline autocompletions.
CRITICAL RULES:
\- Output ONLY the NEW text that comes AFTER the user's input
\- NEVER repeat any words from the end of the user's text
\- Keep completions short (1 sentence max)
\- Match the tone and style of the existing text
\- ${preserveMarkdown}\`
maxOutputTokens = 25
break
}
return streamText({
model: gateway('anthropic/claude-haiku-4.5'),
system,
prompt,
maxOutputTokens
}).toTextStreamResponse()
})
\`\`\`

4. Use the composable in the Editor:

```vue [EditorCompletionExample.vue]
<script setup lang="ts">
import type { EditorCustomHandlers, EditorToolbarItem } from '@nuxt/ui'
import { useEditorCompletion } from './EditorUseCompletion'

const editorRef = useTemplateRef('editorRef')

const value = ref(`# AI Completion

This editor demonstrates how to add AI-powered features using the [Vercel AI SDK](https://ai-sdk.dev/). It includes ghost text autocompletion that appears as you type (press Tab to accept) and text transformation actions.

Try selecting some text and using the AI dropdown to fix grammar, extend, or simplify it.`)

const { extension: completionExtension, handlers: aiHandlers, isLoading: aiLoading } = useEditorCompletion(editorRef)

const customHandlers = {
  ...aiHandlers
} satisfies EditorCustomHandlers

const items = computed(() => [[{
  icon: 'i-lucide-sparkles',
  label: 'Improve',
  variant: 'soft',
  loading: aiLoading.value,
  content: {
    align: 'start'
  },
  items: [{
    kind: 'aiFix',
    icon: 'i-lucide-spell-check',
    label: 'Fix spelling & grammar'
  }, {
    kind: 'aiExtend',
    icon: 'i-lucide-unfold-vertical',
    label: 'Extend text'
  }, {
    kind: 'aiReduce',
    icon: 'i-lucide-fold-vertical',
    label: 'Reduce text'
  }, {
    kind: 'aiSimplify',
    icon: 'i-lucide-lightbulb',
    label: 'Simplify text'
  }, {
    kind: 'aiContinue',
    icon: 'i-lucide-text',
    label: 'Continue sentence'
  }, {
    kind: 'aiSummarize',
    icon: 'i-lucide-list',
    label: 'Summarize'
  }, {
    icon: 'i-lucide-languages',
    label: 'Translate',
    children: [{
      kind: 'aiTranslate',
      language: 'English',
      label: 'English'
    }, {
      kind: 'aiTranslate',
      language: 'French',
      label: 'French'
    }, {
      kind: 'aiTranslate',
      language: 'Spanish',
      label: 'Spanish'
    }, {
      kind: 'aiTranslate',
      language: 'German',
      label: 'German'
    }]
  }]
}], [{
  icon: 'i-lucide-heading',
  content: {
    align: 'start'
  },
  items: [{
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }]
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline'
}]] satisfies EditorToolbarItem<typeof customHandlers>[][])
</script>

<template>
  <UEditor
    ref="editorRef"
    v-slot="{ editor }"
    v-model="value"
    :extensions="[completionExtension]"
    :handlers="customHandlers"
    content-type="markdown"
    :ui="{ base: 'p-8 sm:px-16' }"
    class="w-full min-h-74"
  >
    <UEditorToolbar :editor="editor" :items="items" class="border-b border-muted py-2 px-8 sm:px-16 overflow-x-auto" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> The completion extension can be configured with \`autoTrigger: true\` to automatically suggest completions while typing (disabled by default). You can also manually trigger it with .

\> \[!NOTE]
\> See: https\://ai-sdk.dev/
\> Learn more about the Vercel AI SDK and available providers.

## API

### Props

```ts
/**
 * Props for the Editor component
 */
interface EditorProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  modelValue?: null | string | JSONContent | JSONContent[];
  /**
   * The content type the content is provided as.
   * When not specified, it's automatically inferred: strings are treated as 'html', objects as 'json'.
   */
  contentType?: EditorContentType | undefined;
  /**
   * The starter kit options to configure the editor.
   */
  starterKit?: Partial<StarterKitOptions> | undefined;
  /**
   * The placeholder text to show in empty paragraphs. Can be a string or PlaceholderOptions from `@tiptap/extension-placeholder`.
   */
  placeholder?: string | (Partial<PlaceholderOptions> & { mode?: "firstLine" | "everyLine" | undefined; }) | undefined;
  /**
   * The markdown extension options to configure markdown parsing and serialization.
   */
  markdown?: Partial<MarkdownExtensionOptions> | undefined;
  /**
   * The image extension options to configure image handling. Set to `false` to disable the extension.
   * @default "true"
   */
  image?: boolean | Partial<ImageOptions> | undefined;
  /**
   * The mention extension options to configure mention handling. Set to `false` to disable the extension.
   * The `suggestion` and `suggestions` options are omitted as they are managed by the `EditorMentionMenu` component.
   * @default "true"
   */
  mention?: boolean | Partial<Omit<MentionOptions<any, MentionNodeAttrs>, "suggestion" | "suggestions">> | undefined;
  /**
   * Custom item handlers to override or extend the default handlers.
   * These handlers are provided to all child components (toolbar, suggestion menu, etc.).
   */
  handlers?: H | undefined;
  ui?: { root?: ClassNameValue; content?: ClassNameValue; base?: ClassNameValue; } | undefined;
  /**
   * The extensions to use
   */
  extensions?: Extensions | undefined;
  /**
   * Whether to inject base CSS styles
   */
  injectCSS?: boolean | undefined;
  /**
   * A nonce to use for CSP while injecting styles
   */
  injectNonce?: string | undefined;
  /**
   * The editor's initial focus position
   */
  autofocus?: FocusPosition | undefined;
  /**
   * Whether the editor is editable
   */
  editable?: boolean | undefined;
  /**
   * The default text direction for all content in the editor.
   * When set to 'ltr' or 'rtl', all nodes will have the corresponding dir attribute.
   * When set to 'auto', the dir attribute will be set based on content detection.
   * When undefined, no dir attribute will be added.
   */
  textDirection?: "ltr" | "rtl" | "auto" | undefined;
  /**
   * The editor's props
   */
  editorProps?: EditorProps<any> | undefined;
  parseOptions?: ParseOptions;
  /**
   * The editor's core extension options
   */
  coreExtensionOptions?: { clipboardTextSerializer?: { blockSeparator?: string | undefined; } | undefined; delete?: { async?: boolean | undefined; filterTransaction?: ((transaction: Transaction) => boolean) | undefined; } | undefined; } | undefined;
  /**
   * Whether to enable input rules behavior
   */
  enableInputRules?: EnableRules | undefined;
  /**
   * Whether to enable paste rules behavior
   */
  enablePasteRules?: EnableRules | undefined;
  /**
   * Determines whether core extensions are enabled.
   * 
   * If set to `false`, all core extensions will be disabled.
   * To disable specific core extensions, provide an object where the keys are the extension names and the values are `false`.
   * Extensions not listed in the object will remain enabled.
   */
  enableCoreExtensions?: boolean | Partial<Record<"editable" | "textDirection" | "clipboardTextSerializer" | "commands" | "focusEvents" | "keymap" | "tabindex" | "drop" | "paste" | "delete", false>> | undefined;
  /**
   * If `true`, the editor will check the content for errors on initialization.
   * Emitting the `contentError` event if the content is invalid.
   * Which can be used to show a warning or error message to the user.
   */
  enableContentCheck?: boolean | undefined;
  /**
   * If `true`, the editor will emit the `contentError` event if invalid content is
   * encountered but `enableContentCheck` is `false`. This lets you preserve the
   * invalid editor content while still showing a warning or error message to
   * the user.
   */
  emitContentError?: boolean | undefined;
  /**
   * Called before the editor is constructed.
   */
  onBeforeCreate?: ((props: { editor: Editor; }) => void) | undefined;
  /**
   * Called after the editor is constructed.
   */
  onCreate?: ((props: { editor: Editor; }) => void) | undefined;
  /**
   * Called when the editor is mounted.
   */
  onMount?: ((props: { editor: Editor; }) => void) | undefined;
  /**
   * Called when the editor is unmounted.
   */
  onUnmount?: ((props: { editor: Editor; }) => void) | undefined;
  /**
   * Called when the editor encounters an error while parsing the content.
   * Only enabled if `enableContentCheck` is `true`.
   */
  onContentError?: ((props: { editor: Editor; error: Error; disableCollaboration: () => void; }) => void) | undefined;
  /**
   * Called when the editor's content is updated.
   */
  onUpdate?: ((props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }) => void) | undefined;
  /**
   * Called when the editor's selection is updated.
   */
  onSelectionUpdate?: ((props: { editor: Editor; transaction: Transaction; }) => void) | undefined;
  /**
   * Called after a transaction is applied to the editor.
   */
  onTransaction?: ((props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }) => void) | undefined;
  /**
   * Called on focus events.
   */
  onFocus?: ((props: { editor: Editor; event: FocusEvent; transaction: Transaction; }) => void) | undefined;
  /**
   * Called on blur events.
   */
  onBlur?: ((props: { editor: Editor; event: FocusEvent; transaction: Transaction; }) => void) | undefined;
  /**
   * Called when the editor is destroyed.
   */
  onDestroy?: ((props: void) => void) | undefined;
  /**
   * Called when content is pasted into the editor.
   */
  onPaste?: ((e: ClipboardEvent, slice: Slice) => void) | undefined;
  /**
   * Called when content is dropped into the editor.
   */
  onDrop?: ((e: DragEvent, slice: Slice, moved: boolean) => void) | undefined;
  /**
   * Called when content is deleted from the editor.
   */
  onDelete?: ((props: { editor: Editor; deletedRange: Range; newRange: Range; transaction: Transaction; combinedTransform: Transform; partial: boolean; from: number; to: number; } & ({ type: "node"; node: Node; newFrom: number; newTo: number; } | { type: "mark"; mark: Mark; })) => void) | undefined;
  /**
   * Whether to enable extension-level dispatching of transactions.
   * If `false`, extensions cannot define their own `dispatchTransaction` hook.
   */
  enableExtensionDispatchTransaction?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Editor component
 */
interface EditorSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Editor component
 */
interface EditorEmits {
  update:modelValue: (payload: [value: T]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                         | Type                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `editor`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<Editor | undefined>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/api/editor
\> The exposed editor instance is the TipTap Editor API. Check the TipTap documentation for all available methods and properties.

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editor: {
      slots: {
        root: '',
        content: 'relative size-full flex-1',
        base: [
          'w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-primary/20',
          '[&_p]:leading-7',
          '[&_a]:text-primary [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-primary [&_a]:font-medium',
          '[&_a]:transition-colors',
          '[&_a>code]:border-dashed [&_a:hover>code]:border-primary [&_a:hover>code]:text-primary',
          '[&_a>code]:transition-colors',
          '[&_.mention]:text-primary [&_.mention]:font-medium',
          '[&_:is(h1,h2,h3,h4,h5,h6)]:text-highlighted [&_:is(h1,h2,h3,h4,h5,h6)]:font-bold',
          '[&_h1]:text-3xl',
          '[&_h2]:text-2xl',
          '[&_h3]:text-xl',
          '[&_h4]:text-lg',
          '[&_h5]:text-base',
          '[&_h6]:text-base',
          '[&_:is(h1,h2,h3,h4,h5,h6)>code]:border-dashed [&_:is(h1,h2,h3,h4,h5,h6)>code]:font-bold',
          '[&_h2>code]:text-xl/6',
          '[&_h3>code]:text-lg/5',
          '[&_blockquote]:border-s-4 [&_blockquote]:border-accented [&_blockquote]:ps-4 [&_blockquote]:italic',
          '[&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2',
          '[&_hr]:border-t [&_hr]:border-default',
          '[&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-muted [&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-x-auto',
          '[&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent',
          '[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono [&_code]:font-medium [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-muted [&_code]:text-highlighted [&_code]:bg-muted',
          '[&_:is(ul,ol)]:ps-6',
          '[&_ul]:list-disc [&_ul]:marker:text-(--ui-border-accented)',
          '[&_ol]:list-decimal [&_ol]:marker:text-muted',
          '[&_li]:my-1.5 [&_li]:ps-1.5',
          '[&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-primary',
          '[&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-primary/20'
        ]
      },
      variants: {
        placeholderMode: {
          firstLine: {
            base: '[&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:text-dimmed [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:float-start [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:h-0 [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:pointer-events-none'
          },
          everyLine: {
            base: '[&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:text-dimmed [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:float-start [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:pointer-events-none'
          }
        }
      },
      defaultVariants: {
        placeholderMode: 'everyLine'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Editor.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor.ts).


# EditorDragHandle

## Usage

The EditorDragHandle component provides drag-and-drop functionality for reordering editor blocks using the `@tiptap/extension-drag-handle-vue-3` package.

\> \[!CAUTION]
\> It must be used inside an \[Editor]\(/docs/components/editor) component's default slot to have access to the editor instance.

It extends the [Button](https://ui.nuxt.com/docs/components/button) component, so you can pass any property such as `color`, `variant`, `size`, etc.

```vue [EditorDragHandleExample.vue]
<script setup lang="ts">
const value = ref(`# Drag Handle

Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-21"
  >
    <UEditorDragHandle :editor="editor" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/functionality/drag-handle-vue
\> Learn more about the Drag Handle extension in the TipTap documentation.

### Icon

Use the `icon` prop to customize the drag handle icon.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorDragHandle :editor="editor" icon="i-lucide-move" />
  </UEditor>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.drag\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.drag\` key.

### Options

Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options){rel="&#x22;nofollow&#x22;"}.

\> \[!NOTE]
\> The offset is automatically calculated to center the handle for small blocks and align it to the top for taller blocks.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorDragHandle
      :editor="editor"
      :options="{
        placement: 'left'
      }"
    />
  </UEditor>
</template>
```

## Examples

### With dropdown menu

Use the default slot to add a [DropdownMenu](https://ui.nuxt.com/docs/components/dropdown-menu) with block-level actions like duplicate, delete, move up/down, or transform blocks into different types.

Listen to the `@node-change` event to track the currently hovered node and its position, then use `editor.chain().setMeta('lockDragHandle', open).run()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} to lock the handle position while the menu is open.

```vue [EditorDragHandleDropdownMenuExample.vue]
<script setup lang="ts">
import { upperFirst } from 'scule'
import type { DropdownMenuItem } from '@nuxt/ui'
import { mapEditorItems } from '@nuxt/ui/utils/editor'
import type { Editor, JSONContent } from '@tiptap/vue-3'

const value = ref(`Hover over the left side to see both drag handle and menu button.

Click the menu to see block actions. Try duplicating or deleting a block.`)

const selectedNode = ref<{ node: JSONContent, pos: number }>()

const items = (editor: Editor): DropdownMenuItem[][] => {
  if (!selectedNode.value?.node?.type) {
    return []
  }

  return mapEditorItems(editor, [[
    {
      type: 'label',
      label: upperFirst(selectedNode.value.node.type)
    },
    {
      label: 'Turn into',
      icon: 'i-lucide-repeat-2',
      children: [
        { kind: 'paragraph', label: 'Paragraph', icon: 'i-lucide-type' },
        { kind: 'heading', level: 1, label: 'Heading 1', icon: 'i-lucide-heading-1' },
        { kind: 'heading', level: 2, label: 'Heading 2', icon: 'i-lucide-heading-2' },
        { kind: 'heading', level: 3, label: 'Heading 3', icon: 'i-lucide-heading-3' },
        { kind: 'heading', level: 4, label: 'Heading 4', icon: 'i-lucide-heading-4' },
        { kind: 'bulletList', label: 'Bullet List', icon: 'i-lucide-list' },
        { kind: 'orderedList', label: 'Ordered List', icon: 'i-lucide-list-ordered' },
        { kind: 'blockquote', label: 'Blockquote', icon: 'i-lucide-text-quote' },
        { kind: 'codeBlock', label: 'Code Block', icon: 'i-lucide-square-code' }
      ]
    },
    {
      kind: 'clearFormatting',
      pos: selectedNode.value?.pos,
      label: 'Reset formatting',
      icon: 'i-lucide-rotate-ccw'
    }
  ], [
    {
      kind: 'duplicate',
      pos: selectedNode.value?.pos,
      label: 'Duplicate',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Copy to clipboard',
      icon: 'i-lucide-clipboard',
      onSelect: async () => {
        if (!selectedNode.value) return

        const pos = selectedNode.value.pos
        const node = editor.state.doc.nodeAt(pos)
        if (node) {
          await navigator.clipboard.writeText(node.textContent)
        }
      }
    }
  ], [
    {
      kind: 'moveUp',
      pos: selectedNode.value?.pos,
      label: 'Move up',
      icon: 'i-lucide-arrow-up'
    },
    {
      kind: 'moveDown',
      pos: selectedNode.value?.pos,
      label: 'Move down',
      icon: 'i-lucide-arrow-down'
    }
  ], [
    {
      kind: 'delete',
      pos: selectedNode.value?.pos,
      label: 'Delete',
      icon: 'i-lucide-trash'
    }
  ]]) as DropdownMenuItem[][]
}
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-19"
  >
    <UEditorDragHandle v-slot="{ ui }" :editor="editor" @node-change="selectedNode = $event">
      <UDropdownMenu
        v-slot="{ open }"
        :modal="false"
        :items="items(editor)"
        :content="{ side: 'left' }"
        :ui="{ content: 'w-48', label: 'text-xs' }"
        @update:open="editor.chain().setMeta('lockDragHandle', $event).run()"
      >
        <UButton
          icon="i-lucide-grip-vertical"
          color="neutral"
          variant="ghost"
          active-variant="soft"
          size="sm"
          :active="open"
          :class="ui.handle()"
        />
      </UDropdownMenu>
    </UEditorDragHandle>
  </UEditor>
</template>
```

\> \[!NOTE]
\> This example uses the \`mapEditorItems\` utility from \`@nuxt/ui/utils/editor\` to automatically map handler kinds (like \`duplicate\`, \`delete\`, \`moveUp\`, etc.) to their corresponding editor commands with proper state management.

### With suggestion menu

Use the default slot to add a [Button](https://ui.nuxt.com/docs/components/button) next to the drag handle to open the [EditorSuggestionMenu](https://ui.nuxt.com/docs/components/editor-suggestion-menu).

Call the `onClick` slot function to get the current node position, then use `handlers.suggestion?.execute(editor, { pos: node?.pos }).run()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} to insert new blocks at that position.

```vue [EditorDragHandleSuggestionMenuExample.vue]
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'

const value = ref(`Click the plus button to open the suggestion menu and add new blocks.

The button appears when hovering over blocks.`)

const suggestionItems: EditorSuggestionMenuItem[][] = [[{
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor, handlers }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-35"
    :ui="{ base: 'p-8 sm:px-16' }"
  >
    <UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />

    <UEditorDragHandle v-slot="{ ui, onClick }" :editor="editor">
      <UButton
        icon="i-lucide-plus"
        color="neutral"
        variant="ghost"
        size="sm"
        :class="ui.handle()"
        @click="(e) => {
          e.stopPropagation()

          const selected = onClick()
          handlers.suggestion?.execute(editor, { pos: selected?.pos }).run()
        }"
      />

      <UButton
        icon="i-lucide-grip-vertical"
        color="neutral"
        variant="ghost"
        size="sm"
        :class="ui.handle()"
      />
    </UEditorDragHandle>
  </UEditor>
</template>
```

## API

### Props

```ts
/**
 * Props for the EditorDragHandle component
 */
interface EditorDragHandleProps {
  editor?: Editor;
  icon?: any;
  /**
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * @default "\"ghost\""
   */
  variant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  /**
   * The options for positioning the drag handle. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware.
   */
  options?: FloatingUIOptions | undefined;
  ui?: ({ root?: ClassNameValue; handle?: ClassNameValue; } & { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; }) | undefined;
  pluginKey?: string | PluginKey<any> | undefined;
  nestedOptions?: NormalizedNestedOptions | undefined;
  onElementDragStart?: ((e: DragEvent) => void) | undefined;
  onElementDragEnd?: ((e: DragEvent) => void) | undefined;
  getReferencedVirtualElement?: (() => VirtualElement | null) | undefined;
  /**
   * Enable drag handles for nested content (list items, blockquotes, etc.).
   * 
   * When enabled, the drag handle will appear for nested blocks, not just
   * top-level blocks. A rule-based scoring system determines which node
   * to target based on cursor position and configured rules.
   */
  nested?: boolean | NestedOptions | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  autofocus?: Booleanish | undefined;
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  name?: string | undefined;
  /**
   * The type of the button when not a link.
   */
  type?: "reset" | "submit" | "button" | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | ((event: MouseEvent) => void | Promise<void>)[] | undefined;
  /**
   * The element or component this component should render as when not a link.
   */
  as?: any;
  label?: string | undefined;
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  activeVariant?: "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | undefined;
  /**
   * @default "\"sm\""
   */
  size?: "sm" | "xs" | "md" | "lg" | "xl" | undefined;
  /**
   * Render the button with equal padding on all sides.
   */
  square?: boolean | undefined;
  /**
   * Render the button full width.
   */
  block?: boolean | undefined;
  /**
   * Set loading state automatically based on the `@click` promise state
   */
  loadingAuto?: boolean | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

### Slots

```ts
/**
 * Slots for the EditorDragHandle component
 */
interface EditorDragHandleSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the EditorDragHandle component
 */
interface EditorDragHandleEmits {
  nodeChange: (payload: [{ node: JSONContent; pos: number; }]) => void;
  hover: (payload: [{ node: JSONContent; pos: number; }]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editorDragHandle: {
      slots: {
        root: 'hidden sm:flex items-center justify-center transition-all duration-200 ease-out',
        handle: 'cursor-grab px-1'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/EditorDragHandle.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor-drag-handle.ts).


# EditorEmojiMenu

## Usage

The EditorEmojiMenu component displays a menu of emoji suggestions when typing the `:` character in the editor and inserts the selected emoji. It works alongside the `@tiptap/extension-emoji` package to provide emoji support.

\> \[!NOTE]
\> It uses the \`useEditorMenu\` composable built on top of TipTap's \[Suggestion]\(https\://tiptap.dev/docs/editor/api/utilities/suggestion) utility to filter items as you type and support keyboard navigation (arrow keys, enter to select, escape to close).

\> \[!CAUTION]
\> It must be used inside an \[Editor]\(/docs/components/editor) component's default slot to have access to the editor instance.

```vue [EditorEmojiMenuExample.vue]
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'

const value = ref(`# Emoji Menu

Type : to insert emojis and select from the list of available emojis.`)

const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(emoji => !emoji.name.startsWith('regional_indicator_'))

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[Emoji]"
    content-type="markdown"
    placeholder="Type : to add emojis..."
    class="w-full min-h-21"
  >
    <UEditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

\> \[!WARNING]
\> The \`@tiptap/extension-emoji\` package is not installed by default, you need to install it separately.

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/nodes/emoji
\> Learn more about the Emoji extension in the TipTap documentation.

### Items

Use the `items` prop as an array of objects with the following properties:

- `name: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `emoji: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `shortcodes?: string[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `tags?: string[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `group?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `fallbackImage?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [EditorEmojiMenuItemsExample.vue]
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji } from '@tiptap/extension-emoji'

const value = ref(`Type : to see a custom emoji set.

You can also install the \`@tiptap/extension-emoji\` extension to use a comprehensive set with over 1800 emojis.`)

const items: EditorEmojiMenuItem[] = [{
  name: 'smile',
  emoji: '😄',
  shortcodes: ['smile'],
  tags: ['happy', 'joy', 'pleased']
}, {
  name: 'heart',
  emoji: '❤️',
  shortcodes: ['heart'],
  tags: ['love', 'like']
}, {
  name: 'thumbsup',
  emoji: '👍',
  shortcodes: ['thumbsup', '+1'],
  tags: ['approve', 'ok']
}, {
  name: 'fire',
  emoji: '🔥',
  shortcodes: ['fire'],
  tags: ['hot', 'burn']
}, {
  name: 'rocket',
  emoji: '🚀',
  shortcodes: ['rocket'],
  tags: ['ship', 'launch']
}, {
  name: 'eyes',
  emoji: '👀',
  shortcodes: ['eyes'],
  tags: ['look', 'watch']
}, {
  name: 'tada',
  emoji: '🎉',
  shortcodes: ['tada'],
  tags: ['party', 'celebration']
}, {
  name: 'thinking',
  emoji: '🤔',
  shortcodes: ['thinking'],
  tags: ['hmm', 'think', 'consider']
}]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[Emoji]"
    content-type="markdown"
    placeholder="Type : to add emojis..."
    class="w-full min-h-26"
  >
    <UEditorEmojiMenu :editor="editor" :items="items" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

### Char

Use the `char` prop to change the trigger character. Defaults to `:`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorEmojiMenu :editor="editor" :items="items" char=";" />
  </UEditor>
</template>
```

### Options

Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options){rel="&#x22;nofollow&#x22;"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorEmojiMenu
      :editor="editor"
      :items="items"
      :options="{
        placement: 'bottom-start',
        offset: 4
      }"
    />
  </UEditor>
</template>
```

## API

### Props

```ts
/**
 * Props for the EditorEmojiMenu component
 */
interface EditorEmojiMenuProps {
  size?: "xs" | "md" | "sm" | "lg" | "xl" | undefined;
  items?: T[] | T[][] | undefined;
  ui?: { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } | undefined;
  editor?: Editor;
  /**
   * The trigger character (e.g., '/', '@', ':')
   * @default "\":\""
   */
  char?: string | undefined;
  /**
   * Plugin key to identify this menu
   * @default "\"emojiMenu\""
   */
  pluginKey?: string | undefined;
  /**
   * Fields to filter items by.
   * @default "[\"name\", \"shortcodes\", \"tags\"]"
   */
  filterFields?: string[] | undefined;
  /**
   * Maximum number of items to display
   */
  limit?: number | undefined;
  /**
   * The options for positioning the menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware.
   */
  options?: FloatingUIOptions | undefined;
  /**
   * The DOM element to append the menu to. Default is the editor's parent element.
   * 
   * Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.
   */
  appendTo?: HTMLElement | (() => HTMLElement) | undefined;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editorEmojiMenu: {
      slots: {
        content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0 flex items-center justify-center',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
      },
      variants: {
        size: {
          xs: {
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          sm: {
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          md: {
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          lg: {
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          xl: {
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6 text-xl',
            itemLeadingAvatarSize: 'xs'
          }
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated/75',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
              'transition-colors'
            ]
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/EditorEmojiMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor-emoji-menu.ts).


# EditorMentionMenu

## Usage

The EditorMentionMenu component displays a menu of user suggestions when typing a trigger character (defaults to `@`) in the editor and inserts the selected mention using the `@tiptap/extension-mention` package. The trigger character is also used as the prefix when rendering the inserted mention.

\> \[!NOTE]
\> It uses the \`useEditorMenu\` composable built on top of TipTap's \[Suggestion]\(https\://tiptap.dev/docs/editor/api/utilities/suggestion) utility to filter items as you type and support keyboard navigation (arrow keys, enter to select, escape to close).

\> \[!CAUTION]
\> It must be used inside an \[Editor]\(/docs/components/editor) component's default slot to have access to the editor instance.

```vue [EditorMentionMenuExample.vue]
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'

const value = ref(`# Mention Menu

Type @ to mention someone and select from the list of available users.`)

const items: EditorMentionMenuItem[] = [{
  label: 'benjamincanac',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/739984?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'atinux',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/904724?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'danielroe',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/28706372?v=4',
    loading: 'lazy' as const
  }
}, {
  label: 'pi0',
  avatar: {
    src: 'https://avatars.githubusercontent.com/u/5158436?v=4',
    loading: 'lazy' as const
  }
}]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention someone..."
    class="w-full min-h-21"
  >
    <UEditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> See: https\://tiptap.dev/docs/editor/extensions/nodes/mention
\> Learn more about the Mention extension in the TipTap documentation.

### Items

Use the `items` prop as an array of objects with the following properties:

- `label: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [EditorMentionMenuItemsExample.vue]
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'

const value = ref(`Type @ to mention a user.

You can customize the items with avatars, icons, and descriptions.`)

const items: EditorMentionMenuItem[] = [{
  label: 'benjamincanac',
  avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4', loading: 'lazy' as const }
}, {
  label: 'HugoRCD',
  avatar: { src: 'https://avatars.githubusercontent.com/u/71938701?v=4', loading: 'lazy' as const }
}, {
  label: 'romhml',
  avatar: { src: 'https://avatars.githubusercontent.com/u/25613751?v=4', loading: 'lazy' as const }
}, {
  label: 'sandros94',
  avatar: { src: 'https://avatars.githubusercontent.com/u/13056429?v=4', loading: 'lazy' as const }
}, {
  label: 'hywax',
  avatar: { src: 'https://avatars.githubusercontent.com/u/149865959?v=4', loading: 'lazy' as const }
}, {
  label: 'J-Michalek',
  avatar: { src: 'https://avatars.githubusercontent.com/u/71264422?v=4', loading: 'lazy' as const }
}, {
  label: 'genu',
  avatar: { src: 'https://avatars.githubusercontent.com/u/928780?v=4', loading: 'lazy' as const }
}]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention..."
    class="w-full min-h-19"
  >
    <UEditorMentionMenu :editor="editor" :items="items" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

### Char

Use the `char` prop to change the trigger character. Defaults to `@`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}. The trigger character is also used as the prefix when rendering the inserted mention (e.g. `#channel` instead of `@channel`).

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorMentionMenu :editor="editor" :items="channels" char="#" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> You can use multiple \`EditorMentionMenu\` components on the same editor with different \`char\` and \`plugin-key\` props to support different mention types.
\> \`\`\`vue
\> \<template>
\> \<UEditor v-slot="{ editor }">
\> \<UEditorMentionMenu \:editor="editor" \:items="users" plugin-key="mentionMenu" />
\> \<UEditorMentionMenu \:editor="editor" \:items="tags" char="#" plugin-key="tagMenu" />
\> \</UEditor>
\> \</template>
\>
\> \`\`\`

### Options

Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options){rel="&#x22;nofollow&#x22;"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorMentionMenu
      :editor="editor"
      :items="items"
      :options="{
        placement: 'bottom-start',
        offset: 4
      }"
    />
  </UEditor>
</template>
```

## Examples

### With ignore filter `4.4+`

You can set the `ignore-filter` prop to `true` to disable the internal search and use your own search logic. Use `v-model:search-term` to access the current search term and fetch items from an API.

```vue [EditorMentionMenuIgnoreFilterExample.vue]
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'

const value = ref(`# Async Mention Menu

Type @ to mention someone. Results are fetched from an API as you type.`)

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: items } = useLazyFetch('https://dummyjson.com/users/search?limit=10', {
  key: 'editor-mention-users-search',
  params: { q: searchTermDebounced },
  transform: (data: { users: { id: number, firstName: string, lastName: string, image: string }[] }) => {
    return data.users?.map(user => ({ id: user.id, label: `${user.firstName} ${user.lastName}`, avatar: { src: user.image, loading: 'lazy' as const } })) || []
  },
  server: false
})

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention someone..."
    class="w-full min-h-21"
  >
    <UEditorMentionMenu
      v-model:search-term="searchTerm"
      :editor="editor"
      :items="items"
      :append-to="appendToBody"
      ignore-filter
    />
  </UEditor>
</template>
```

\> \[!NOTE]
\> This example uses \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/) to debounce the API calls.

## API

### Props

```ts
/**
 * Props for the EditorMentionMenu component
 */
interface EditorMentionMenuProps {
  size?: "xs" | "md" | "sm" | "lg" | "xl" | undefined;
  items?: T[] | T[][] | undefined;
  ui?: { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } | undefined;
  editor?: Editor;
  /**
   * The trigger character (e.g., '/', '@', ':')
   * @default "\"@\""
   */
  char?: string | undefined;
  /**
   * Plugin key to identify this menu
   * @default "\"mentionMenu\""
   */
  pluginKey?: string | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * Maximum number of items to display
   */
  limit?: number | undefined;
  /**
   * The options for positioning the menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware.
   */
  options?: FloatingUIOptions | undefined;
  /**
   * The DOM element to append the menu to. Default is the editor's parent element.
   * 
   * Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.
   */
  appendTo?: HTMLElement | (() => HTMLElement) | undefined;
  /**
   * Whether to ignore the default filtering.
   * When `true`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
   */
  ignoreFilter?: boolean | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editorMentionMenu: {
      slots: {
        content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0 flex items-center justify-center',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
      },
      variants: {
        size: {
          xs: {
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          sm: {
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          md: {
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          lg: {
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          xl: {
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6 text-xl',
            itemLeadingAvatarSize: 'xs'
          }
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated/75',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
              'transition-colors'
            ]
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/EditorMentionMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor-mention-menu.ts).


# EditorSuggestionMenu

## Usage

The EditorSuggestionMenu component displays a menu of formatting and action suggestions when typing a trigger character in the editor and executes the corresponding [handler](https://ui.nuxt.com/docs/components/editor#handlers) when an item is selected.

\> \[!NOTE]
\> It uses the \`useEditorMenu\` composable built on top of TipTap's \[Suggestion]\(https\://tiptap.dev/docs/editor/api/utilities/suggestion) utility to filter items as you type and support keyboard navigation (arrow keys, enter to select, escape to close).

\> \[!CAUTION]
\> It must be used inside an \[Editor]\(/docs/components/editor) component's default slot to have access to the editor instance.

```vue [EditorSuggestionMenuExample.vue]
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'

const value = ref(`# Suggestion Menu

Type / to open the suggestion menu and browse available formatting commands.`)

const items: EditorSuggestionMenuItem[][] = [[{
  type: 'label',
  label: 'Text'
}, {
  kind: 'paragraph',
  label: 'Paragraph',
  icon: 'i-lucide-type'
}, {
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'heading',
  level: 3,
  label: 'Heading 3',
  icon: 'i-lucide-heading-3'
}], [{
  type: 'label',
  label: 'Lists'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  label: 'Numbered List',
  icon: 'i-lucide-list-ordered'
}], [{
  type: 'label',
  label: 'Insert'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}, {
  kind: 'codeBlock',
  label: 'Code Block',
  icon: 'i-lucide-square-code'
}, {
  kind: 'horizontalRule',
  label: 'Divider',
  icon: 'i-lucide-separator-horizontal'
}]]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type / for commands..."
    class="w-full min-h-21"
  >
    <UEditorSuggestionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </UEditor>
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- [`kind?: "textAlign" | "heading" | "link" | "image" | "blockquote" | "bulletList" | "orderedList" | "taskList" | "codeBlock" | "horizontalRule" | "paragraph" | "clearFormatting" | "duplicate" | "delete" | "moveUp" | "moveDown" | "suggestion" | "mention" | "emoji"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/docs/components/editor#handlers)
- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `type?: "label" | "separator"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [EditorSuggestionMenuItemsExample.vue]
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'

const value = ref(`Type / to see a list of commands.

You can customize the items with icons, labels, and descriptions.`)

const items: EditorSuggestionMenuItem[][] = [[{
  type: 'label',
  label: 'Text Styles'
}, {
  kind: 'paragraph',
  label: 'Paragraph',
  icon: 'i-lucide-type'
}, {
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'heading',
  level: 3,
  label: 'Heading 3',
  icon: 'i-lucide-heading-3'
}], [{
  type: 'label',
  label: 'Lists'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  label: 'Numbered List',
  icon: 'i-lucide-list-ordered'
}], [{
  type: 'label',
  label: 'Blocks'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}, {
  kind: 'codeBlock',
  label: 'Code Block',
  icon: 'i-lucide-square-code'
}, {
  kind: 'horizontalRule',
  label: 'Divider',
  icon: 'i-lucide-separator-horizontal'
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type / for commands..."
    class="w-full min-h-19"
  >
    <UEditorSuggestionMenu :editor="editor" :items="items" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

\> \[!TIP]
\> Use \`type: 'label'\` for section headers and \`type: 'separator'\` for visual dividers to organize commands into logical groups for better discoverability.

### Char

Use the `char` prop to change the trigger character. Defaults to `/`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorSuggestionMenu :editor="editor" :items="items" char=">" />
  </UEditor>
</template>
```

### Options

Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options){rel="&#x22;nofollow&#x22;"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorSuggestionMenu
      :editor="editor"
      :items="items"
      :options="{
        placement: 'bottom-start',
        offset: 4
      }"
    />
  </UEditor>
</template>
```

## API

### Props

```ts
/**
 * Props for the EditorSuggestionMenu component
 */
interface EditorSuggestionMenuProps {
  size?: "xs" | "md" | "sm" | "lg" | "xl" | undefined;
  items?: T[] | T[][] | undefined;
  ui?: { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } | undefined;
  editor?: Editor;
  /**
   * The trigger character (e.g., '/', '@', ':')
   * @default "\"/\""
   */
  char?: string | undefined;
  /**
   * Plugin key to identify this menu
   * @default "\"suggestionMenu\""
   */
  pluginKey?: string | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * Maximum number of items to display
   */
  limit?: number | undefined;
  /**
   * The options for positioning the menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware.
   */
  options?: FloatingUIOptions | undefined;
  /**
   * The DOM element to append the menu to. Default is the editor's parent element.
   * 
   * Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.
   */
  appendTo?: HTMLElement | (() => HTMLElement) | undefined;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editorSuggestionMenu: {
      slots: {
        content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
        itemLeadingIcon: 'shrink-0 flex items-center justify-center',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
      },
      variants: {
        size: {
          xs: {
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          sm: {
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4 text-sm',
            itemLeadingAvatarSize: '3xs'
          },
          md: {
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          lg: {
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5 text-base',
            itemLeadingAvatarSize: '2xs'
          },
          xl: {
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6 text-xl',
            itemLeadingAvatarSize: 'xs'
          }
        },
        active: {
          true: {
            item: 'text-highlighted before:bg-elevated/75',
            itemLeadingIcon: 'text-default'
          },
          false: {
            item: [
              'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
              'transition-colors'
            ]
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/EditorSuggestionMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor-suggestion-menu.ts).


# EditorToolbar

## Usage

The EditorToolbar component displays a toolbar of formatting buttons that automatically sync their active state with the editor content. It supports three layout modes using the `@tiptap/vue-3/menus` package:

- `fixed`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} (always visible)
- `bubble`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} (appears on text selection)
- `floating`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} (appears on empty lines)

\> \[!CAUTION]
\> It must be used inside an \[Editor]\(/docs/components/editor) component's default slot to have access to the editor instance.

```vue [EditorToolbarExample.vue]
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: EditorToolbarItem[][] = [[{
  icon: 'i-lucide-heading',
  tooltip: { text: 'Headings' },
  content: {
    align: 'start'
  },
  items: [{
    kind: 'heading',
    level: 1,
    icon: 'i-lucide-heading-1',
    label: 'Heading 1'
  }, {
    kind: 'heading',
    level: 2,
    icon: 'i-lucide-heading-2',
    label: 'Heading 2'
  }, {
    kind: 'heading',
    level: 3,
    icon: 'i-lucide-heading-3',
    label: 'Heading 3'
  }, {
    kind: 'heading',
    level: 4,
    icon: 'i-lucide-heading-4',
    label: 'Heading 4'
  }]
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold',
  tooltip: { text: 'Bold' }
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic',
  tooltip: { text: 'Italic' }
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline',
  tooltip: { text: 'Underline' }
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough',
  tooltip: { text: 'Strikethrough' }
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code',
  tooltip: { text: 'Code' }
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-21"
  >
    <UEditorToolbar :editor="editor" :items="items" layout="bubble" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> The bubble and floating layouts use TipTap's \[BubbleMenu]\(https\://tiptap.dev/docs/editor/extensions/functionality/bubble-menu) and \[FloatingMenu]\(https\://tiptap.dev/docs/editor/extensions/functionality/floating-menu) extensions.

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `activeColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `variant?: "solid" | "outline" | "soft" | "ghost" | "link" | "subtle"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `activeVariant?: "solid" | "outline" | "soft" | "ghost" | "link" | "subtle"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `size?: "xs" | "sm" | "md" | "lg" | "xl"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`kind?: "mark" | "textAlign" | "heading" | "link" | "image" | "blockquote" | "bulletList" | "orderedList" | "taskList" | "codeBlock" | "horizontalRule" | "paragraph" | "undo" | "redo" | "clearFormatting" | "duplicate" | "delete" | "moveUp" | "moveDown" | "suggestion" | "mention" | "emoji"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/docs/components/editor#handlers)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `loading?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `active?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `tooltip?: TooltipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-link-popover)
- `onClick?: (e: MouseEvent) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `items?: EditorToolbarItem[] | EditorToolbarItem[][]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button#props) component such as `color`, `variant`, `size`, etc.

```vue [EditorToolbarItemsExample.vue]
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'
import { TextAlign } from '@tiptap/extension-text-align'

const value = ref(`This toolbar showcases **all available formatting options** using built-in handlers. Try the different controls to see them in action!

You can apply **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, and \`inline code\` formatting to your text.
`)

const items: EditorToolbarItem[][] = [
  // History controls
  [{
    kind: 'undo',
    icon: 'i-lucide-undo',
    tooltip: { text: 'Undo' }
  }, {
    kind: 'redo',
    icon: 'i-lucide-redo',
    tooltip: { text: 'Redo' }
  }],
  // Block types
  [{
    icon: 'i-lucide-heading',
    tooltip: { text: 'Headings' },
    content: {
      align: 'start'
    },
    items: [{
      kind: 'heading',
      level: 1,
      icon: 'i-lucide-heading-1',
      label: 'Heading 1'
    }, {
      kind: 'heading',
      level: 2,
      icon: 'i-lucide-heading-2',
      label: 'Heading 2'
    }, {
      kind: 'heading',
      level: 3,
      icon: 'i-lucide-heading-3',
      label: 'Heading 3'
    }, {
      kind: 'heading',
      level: 4,
      icon: 'i-lucide-heading-4',
      label: 'Heading 4'
    }]
  }, {
    icon: 'i-lucide-list',
    tooltip: { text: 'Lists' },
    content: {
      align: 'start'
    },
    items: [{
      kind: 'bulletList',
      icon: 'i-lucide-list',
      label: 'Bullet List'
    }, {
      kind: 'orderedList',
      icon: 'i-lucide-list-ordered',
      label: 'Ordered List'
    }]
  }, {
    kind: 'blockquote',
    icon: 'i-lucide-text-quote',
    tooltip: { text: 'Blockquote' }
  }, {
    kind: 'codeBlock',
    icon: 'i-lucide-square-code',
    tooltip: { text: 'Code Block' }
  }, {
    kind: 'horizontalRule',
    icon: 'i-lucide-separator-horizontal',
    tooltip: { text: 'Horizontal Rule' }
  }],
  // Text formatting
  [{
    kind: 'mark',
    mark: 'bold',
    icon: 'i-lucide-bold',
    tooltip: { text: 'Bold' }
  }, {
    kind: 'mark',
    mark: 'italic',
    icon: 'i-lucide-italic',
    tooltip: { text: 'Italic' }
  }, {
    kind: 'mark',
    mark: 'underline',
    icon: 'i-lucide-underline',
    tooltip: { text: 'Underline' }
  }, {
    kind: 'mark',
    mark: 'strike',
    icon: 'i-lucide-strikethrough',
    tooltip: { text: 'Strikethrough' }
  }, {
    kind: 'mark',
    mark: 'code',
    icon: 'i-lucide-code',
    tooltip: { text: 'Code' }
  }],
  // Link
  [{
    kind: 'link',
    icon: 'i-lucide-link',
    tooltip: { text: 'Link' }
  }],
  // Text alignment
  [{
    icon: 'i-lucide-align-justify',
    tooltip: { text: 'Text Align' },
    content: {
      align: 'end'
    },
    items: [{
      kind: 'textAlign',
      align: 'left',
      icon: 'i-lucide-align-left',
      label: 'Align Left'
    }, {
      kind: 'textAlign',
      align: 'center',
      icon: 'i-lucide-align-center',
      label: 'Align Center'
    }, {
      kind: 'textAlign',
      align: 'right',
      icon: 'i-lucide-align-right',
      label: 'Align Right'
    }, {
      kind: 'textAlign',
      align: 'justify',
      icon: 'i-lucide-align-justify',
      label: 'Align Justify'
    }]
  }]
]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]"
    class="w-full min-h-37 flex flex-col gap-4"
  >
    <UEditorToolbar :editor="editor" :items="items" class="sm:px-8 overflow-x-auto" />
  </UEditor>
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to create separated groups of items.

\> \[!TIP]
\> Each item can take an \`items\` array of objects with the same properties as the \`items\` prop to create a \[DropdownMenu]\(/docs/components/dropdown-menu).

### Layout

Use the `layout` prop to change how the toolbar is displayed. Defaults to `fixed`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

```vue [EditorToolbarLayoutExample.vue]
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

defineProps<{
  layout: 'fixed' | 'bubble' | 'floating'
}>()

const value = ref(`Switch between layouts to see the different toolbar modes.

The **fixed** layout displays the toolbar above the editor. The **bubble** layout shows the toolbar when you select text. The **floating** layout appears on empty lines.`)

const items: EditorToolbarItem[][] = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-26 flex flex-col gap-4">
    <UEditorToolbar
      :key="layout"
      :editor="editor"
      :items="items"
      :layout="layout"
      :data-layout="layout"
      class="data-[layout=fixed]:sm:px-8"
    />
  </UEditor>
</template>
```

### Options

When using `bubble`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} or `floating`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} layouts, use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options){rel="&#x22;nofollow&#x22;"}.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorToolbar
      :editor="editor"
      :items="items"
      layout="bubble"
      :options="{
        placement: 'top',
        offset: 8,
        flip: { padding: 8 },
        shift: { padding: 8 }
      }"
    />
  </UEditor>
</template>
```

### Should Show

When using `bubble`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} or `floating`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} layouts, use the `should-show` prop to control when the toolbar appears. This function receives context about the editor state and returns a boolean.

```vue
<template>
  <UEditor v-slot="{ editor }">
    <UEditorToolbar
      :editor="editor"
      :items="items"
      layout="bubble"
      :should-show="({ view, state }) => {
        const { selection } = state
        const { from, to } = selection
        const text = state.doc.textBetween(from, to)
        return view.hasFocus() && !selection.empty && text.length > 10
      }"
    />
  </UEditor>
</template>
```

## Examples

### With image toolbar

Use the `should-show` prop to create context-specific toolbars that appear only for certain node types. This example shows a `bubble` toolbar with download and delete actions that only appears when an image is selected.

```vue [EditorToolbarImageExample.vue]
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`Click on the image below to see the image-specific toolbar:

![Image Placeholder](/placeholder.jpeg)`)

const items = (editor: Editor): EditorToolbarItem[][] => {
  const node = editor.state.doc.nodeAt(editor.state.selection.from)

  return [[{
    icon: 'i-lucide-download',
    to: node?.attrs?.src,
    download: true,
    tooltip: { text: 'Download' }
  }], [{
    icon: 'i-lucide-trash',
    tooltip: { text: 'Delete' },
    onClick: () => {
      const { state } = editor
      const { selection } = state

      const pos = selection.from
      const node = state.doc.nodeAt(pos)

      if (node && node.type.name === 'image') {
        editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).run()
      }
    }
  }]]
}
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-113"
  >
    <UEditorToolbar
      :editor="editor"
      :items="items(editor)"
      layout="bubble"
      :should-show="({ editor, view }) => {
        return editor.isActive('image') && view.hasFocus()
      }"
    />
  </UEditor>
</template>
```

### With link popover

This example demonstrates how to create a custom link popover using the `slot` property on toolbar items and the [Popover](https://ui.nuxt.com/docs/components/popover) component.

1. Create a Vue component that wraps a [Popover](https://ui.nuxt.com/docs/components/popover) with link editing functionality:

```vue [EditorLinkPopover.vue]
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'

const props = defineProps<{
  editor: Editor
  autoOpen?: boolean
}>()

const open = ref(false)
const url = ref('')

const active = computed(() => props.editor.isActive('link'))
const disabled = computed(() => {
  if (!props.editor.isEditable) return true
  const { selection } = props.editor.state
  return selection.empty && !props.editor.isActive('link')
})

watch(() => props.editor, (editor, _, onCleanup) => {
  if (!editor) return

  const updateUrl = () => {
    const { href } = editor.getAttributes('link')
    url.value = href || ''
  }

  updateUrl()
  editor.on('selectionUpdate', updateUrl)

  onCleanup(() => {
    editor.off('selectionUpdate', updateUrl)
  })
}, { immediate: true })

watch(active, (isActive) => {
  if (isActive && props.autoOpen) {
    open.value = true
  }
})

function setLink() {
  if (!url.value) return

  const { selection } = props.editor.state
  const isEmpty = selection.empty
  const hasCode = props.editor.isActive('code')

  let chain = props.editor.chain().focus()

  // When linking code, extend the code mark range first to select the full code
  if (hasCode && !isEmpty) {
    chain = chain.extendMarkRange('code').setLink({ href: url.value })
  } else {
    chain = chain.extendMarkRange('link').setLink({ href: url.value })

    if (isEmpty) {
      chain = chain.insertContent({ type: 'text', text: url.value })
    }
  }

  chain.run()
  open.value = false
}

function removeLink() {
  props.editor
    .chain()
    .focus()
    .extendMarkRange('link')
    .unsetLink()
    .setMeta('preventAutolink', true)
    .run()

  url.value = ''
  open.value = false
}

function openLink() {
  if (!url.value) return
  window.open(url.value, '_blank', 'noopener,noreferrer')
}

function handleKeyDown(event: KeyboardEvent) {
  if (event.key === 'Enter') {
    event.preventDefault()
    setLink()
  }
}
</script>

<template>
  <UPopover v-model:open="open" :ui="{ content: 'p-0.5' }">
    <UTooltip text="Link">
      <UButton
        icon="i-lucide-link"
        color="neutral"
        active-color="primary"
        variant="ghost"
        active-variant="soft"
        size="sm"
        :active="active"
        :disabled="disabled"
      />
    </UTooltip>

    <template #content>
      <UInput
        v-model="url"
        autofocus
        name="url"
        type="url"
        variant="none"
        placeholder="Paste a link..."
        @keydown="handleKeyDown"
      >
        <div class="flex items-center mr-0.5">
          <UButton
            icon="i-lucide-corner-down-left"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Apply link"
            @click="setLink"
          />

          <USeparator orientation="vertical" class="h-6 mx-1" />

          <UButton
            icon="i-lucide-external-link"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Open in new window"
            @click="openLink"
          />

          <UButton
            icon="i-lucide-trash"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!url && !active"
            title="Remove link"
            @click="removeLink"
          />
        </div>
      </UInput>
    </template>
  </UPopover>
</template>
```

2. Use the custom component in the toolbar with a named slot:

```vue [EditorToolbarCustomSlotExample.vue]
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'
import EditorLinkPopover from './EditorLinkPopover.vue'

const value = ref(`Select text and click the link button to add a link with the custom popover.

You can also edit existing links like [this one](https://ui.nuxt.com).`)

const toolbarItems = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  slot: 'link' as const
}]] satisfies EditorToolbarItem[][]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-30 flex flex-col gap-4"
  >
    <UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
      <template #link>
        <EditorLinkPopover :editor="editor" auto-open />
      </template>
    </UEditorToolbar>
  </UEditor>
</template>
```

## API

### Props

```ts
/**
 * Props for the EditorToolbar component
 */
interface EditorToolbarProps {
  editor?: Editor;
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The color of the toolbar controls.
   * @default "\"neutral\""
   */
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The variant of the toolbar controls.
   * @default "\"ghost\""
   */
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The color of the active toolbar control.
   * @default "\"primary\""
   */
  activeColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The variant of the active toolbar control.
   * @default "\"soft\""
   */
  activeVariant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The size of the toolbar controls.
   * @default "\"sm\""
   */
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  items?: T | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; group?: ClassNameValue; separator?: ClassNameValue; } | undefined;
  /**
   * @default "\"fixed\""
   */
  layout?: "fixed" | "floating" | "bubble" | undefined;
  /**
   * The plugin key.
   * The plugin key for the floating menu.
   */
  pluginKey?: unknown;
  /**
   * The delay in milliseconds before the menu should be updated.
   * This can be useful to prevent performance issues.
   */
  updateDelay?: unknown;
  /**
   * The delay in milliseconds before the menu position should be updated on window resize.
   * This can be useful to prevent performance issues.
   */
  resizeDelay?: unknown;
  /**
   * A function that determines whether the menu should be shown or not.
   * If this function returns `false`, the menu will be hidden, otherwise it will be shown.
   */
  shouldShow?: unknown;
  /**
   * The DOM element to append your menu to. Default is the editor's parent element.
   * 
   * Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.
   */
  appendTo?: unknown;
  /**
   * A function that returns the virtual element for the menu.
   * This is useful when the menu needs to be positioned relative to a specific DOM element.
   */
  getReferencedVirtualElement?: unknown;
  /**
   * The options for the bubble menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, arrow, size, autoPlacement,
   * hide, and inline middlewares.
   * The options for the floating menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, arrow, size, autoPlacement,
   * hide, and inline middlewares.
   */
  options?: unknown;
}
```

### Slots

```ts
/**
 * Slots for the EditorToolbar component
 */
interface EditorToolbarSlots {
  item(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    editorToolbar: {
      slots: {
        root: 'focus:outline-none',
        base: 'flex items-stretch gap-1.5',
        group: 'flex items-center gap-0.5',
        separator: 'w-px self-stretch bg-border'
      },
      variants: {
        layout: {
          bubble: {
            base: 'bg-default border border-default rounded-lg p-1'
          },
          floating: {
            base: 'bg-default border border-default rounded-lg p-1'
          },
          fixed: {
            base: ''
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/EditorToolbar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/editor-toolbar.ts).


# Empty

## Usage

```vue
<template>
  <u-empty :actions=[{"icon":"i-lucide-plus","label":"Create new"},{"icon":"i-lucide-refresh-cw","label":"Refresh","color":"neutral","variant":"subtle"}] description=It looks like you haven't added any projects. Create one to get started. icon=i-lucide-file title=No projects found />
</template>
```

### Title

Use the `title` prop to set the title of the empty state.

```vue
<template>
  <UEmpty title="No projects found" />
</template>
```

### Description

Use the `description` prop to set the description of the empty state.

```vue
<template>
  <UEmpty title="No projects found" description="It looks like you haven't added any projects. Create one to get started." />
</template>
```

### Icon

Use the `icon` prop to set the icon of the empty state.

```vue
<template>
  <UEmpty icon="i-lucide-file" title="No projects found" description="It looks like you haven't added any projects. Create one to get started." />
</template>
```

### Avatar

Use the `avatar` prop to set the avatar of the empty state.

```vue
<template>
  <UEmpty title="No projects found" description="It looks like you haven't added any projects. Create one to get started." />
</template>
```

### Actions

Use the `actions` prop to add some [Button](https://ui.nuxt.com/docs/components/button) actions to the empty state.

```vue
<template>
  <UEmpty icon="i-lucide-file" title="No projects found" description="It looks like you haven't added any projects. Create one to get started." />
</template>
```

### Variant

Use the `variant` prop to change the variant of the empty state.

```vue
<template>
  <UEmpty variant="naked" icon="i-lucide-bell" title="No notifications" description="You're all caught up. New notifications will appear here." />
</template>
```

### Size

Use the `size` prop to change the size of the empty state.

```vue
<template>
  <UEmpty size="xl" icon="i-lucide-bell" title="No notifications" description="You're all caught up. New notifications will appear here." />
</template>
```

## Examples

### With slots

Use the available slots to create a more complex empty state.

```vue [EmptySlotsExample.vue]
<script setup lang="ts">
import type { UserProps } from '@nuxt/ui'

const members: UserProps[] = [
  {
    name: 'Daniel Roe',
    description: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'danielroe',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Pooya Parsa',
    description: 'pi0',
    to: 'https://github.com/pi0',
    target: '_blank',
    avatar: {
      src: 'https://github.com/pi0.png',
      alt: 'pi0',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Sébastien Chopin',
    description: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png',
      alt: 'atinux',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Benjamin Canac',
    description: 'benjamincanac',
    to: 'https://github.com/benjamincanac',
    target: '_blank',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  }
]
</script>

<template>
  <UEmpty
    title="No team members"
    description="Invite your team to collaborate on this project."
    variant="naked"
    :actions="[{
      label: 'Invite members',
      icon: 'i-lucide-user-plus',
      color: 'neutral'
    }]"
  >
    <template #leading>
      <UAvatarGroup size="xl">
        <UAvatar src="https://github.com/nuxt.png" alt="Nuxt" loading="lazy" />
        <UAvatar src="https://github.com/unjs.png" alt="Unjs" loading="lazy" />
      </UAvatarGroup>
    </template>

    <template #footer>
      <USeparator class="my-4" />

      <div class="grid grid-cols-2 gap-4">
        <UPageCard
          v-for="(member, index) in members"
          :key="index"
          :to="member.to"
          :ui="{ container: 'sm:p-4' }"
        >
          <UUser
            :avatar="member.avatar"
            :name="member.name"
            :description="member.description"
            :ui="{ name: 'truncate' }"
          />
        </UPageCard>
      </div>
    </template>
  </UEmpty>
</template>
```

## API

### Props

```ts
/**
 * Props for the Empty component
 */
interface EmptyProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed above the title.
   */
  icon?: any;
  avatar?: AvatarProps | undefined;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Display a list of Button in the body.
   */
  actions?: ButtonProps[] | undefined;
  variant?: "outline" | "solid" | "soft" | "subtle" | "naked" | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; avatar?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; actions?: ClassNameValue; footer?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Empty component
 */
interface EmptySlots {
  header(): any;
  leading(): any;
  title(): any;
  description(): any;
  body(): any;
  actions(): any;
  footer(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    empty: {
      slots: {
        root: 'relative flex flex-col items-center justify-center gap-4 rounded-lg p-4 sm:p-6 lg:p-8 min-w-0',
        header: 'flex flex-col items-center gap-2 max-w-sm text-center',
        avatar: 'shrink-0 mb-2',
        title: 'text-highlighted text-pretty font-medium',
        description: 'text-balance text-center',
        body: 'flex flex-col items-center gap-4 max-w-sm',
        actions: 'flex flex-wrap justify-center gap-2 shrink-0',
        footer: 'flex flex-col items-center gap-2 max-w-sm'
      },
      variants: {
        size: {
          xs: {
            avatar: 'size-8 text-base',
            title: 'text-sm',
            description: 'text-xs'
          },
          sm: {
            avatar: 'size-9 text-lg',
            title: 'text-sm',
            description: 'text-xs'
          },
          md: {
            avatar: 'size-10 text-xl',
            title: 'text-base',
            description: 'text-sm'
          },
          lg: {
            avatar: 'size-11 text-[22px]',
            title: 'text-base',
            description: 'text-sm'
          },
          xl: {
            avatar: 'size-12 text-2xl',
            title: 'text-lg',
            description: 'text-base'
          }
        },
        variant: {
          solid: {
            root: 'bg-inverted',
            title: 'text-inverted',
            description: 'text-dimmed'
          },
          outline: {
            root: 'bg-default ring ring-default',
            description: 'text-muted'
          },
          soft: {
            root: 'bg-elevated/50',
            description: 'text-toned'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default',
            description: 'text-toned'
          },
          naked: {
            description: 'text-muted'
          }
        }
      },
      defaultVariants: {
        variant: 'outline',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Empty.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/empty.ts).


# Error

## Usage

The Error component renders a `<main>` element that works together with the [Header](https://ui.nuxt.com/docs/components/header) component to create a full-height layout that extends to the viewport's available height.

\> \[!TIP]
\> See: /docs/getting-started/theme/css-variables#header
\> The Error component uses the \`--ui-header-height\` CSS variable to position itself correctly below the \`Header\`.

### Error

Use the `error` prop to display an error message.

\> \[!NOTE]
\> See: https\://nuxt.com/docs/guide/directory-structure/error
\> In most cases, you will receive the \`error\` prop in your \`error.vue\` file.

```vue
<template>
  <UError />
</template>
```

### Clear

Use the `clear` prop to customize or hide the clear button (with `false` value).

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <UError />
</template>
```

### Redirect

Use the `redirect` prop to redirect the user to a different page when the clear button is clicked. Defaults to `/`.

```vue
<template>
  <UError redirect="/docs/getting-started" />
</template>
```

## Examples

### Within `error.vue`

Use the Error component in your `error.vue`:

```vue [error.vue] {13}
<script setup lang="ts">
import type { NuxtError } from '#app'

const props = defineProps<{
  error: NuxtError
}>()
</script>

<template>
  <UApp>
    <UHeader />

    <UError :error="error" />

    <UFooter />
  </UApp>
</template>
```

\> \[!TIP]
\> You might want to replicate the code of your \`app.vue\` inside your \`error.vue\` file to have the same layout and features, here is an example: \[https\://github.com/nuxt/ui/blob/v4/docs/app/error.vue]\(https\://github.com/nuxt/ui/blob/v4/docs/app/error.vue)

\> \[!NOTE]
\> You can read more about how to handle errors in the \[Nuxt documentation]\(https\://nuxt.com/docs/getting-started/error-handling#error-page), but when using \`nuxt generate\` it is recommended to add \`fatal: true\` inside your \`createError\` call to make sure the error page is displayed:
\> \`\`\`vue
\> \<script setup lang="ts">
\> const route = useRoute()
\>
\> const { data: page } = await useAsyncData(route.path, () => {
\> return queryCollection('docs').path(route.path).first()
\> })
\> if (!page.value) {
\> throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
\> }
\> \</script>
\>
\> \`\`\`

## API

### Props

```ts
/**
 * Props for the Error component
 */
interface ErrorProps {
  /**
   * The element or component this component should render as.
   * @default "\"main\""
   */
  as?: any;
  error?: Partial<NuxtError<unknown> & { message: string; }> | undefined;
  /**
   * The URL to redirect to when the error is cleared.
   * @default "\"/\""
   */
  redirect?: string | undefined;
  /**
   * Display a button to clear the error in the links slot.
   * `{ size: 'lg', color: 'primary', variant: 'solid', label: 'Back to home' }`{lang="ts-type"}
   * @default "true"
   */
  clear?: boolean | ButtonProps | undefined;
  ui?: { root?: ClassNameValue; statusCode?: ClassNameValue; statusMessage?: ClassNameValue; message?: ClassNameValue; links?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Error component
 */
interface ErrorSlots {
  default(): any;
  statusCode(): any;
  statusMessage(): any;
  message(): any;
  links(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    error: {
      slots: {
        root: 'min-h-[calc(100vh-var(--ui-header-height))] flex flex-col items-center justify-center text-center',
        statusCode: 'text-base font-semibold text-primary',
        statusMessage: 'mt-2 text-4xl sm:text-5xl font-bold text-highlighted text-balance',
        message: 'mt-4 text-lg text-muted text-balance',
        links: 'mt-8 flex items-center justify-center gap-6'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Error.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/error.ts).


# FieldGroup

## Usage

Wrap multiple [Button](https://ui.nuxt.com/components/button) within a FieldGroup to group them together.

```vue
<template>
  <UFieldGroup>
    <UButton color="neutral" variant="subtle" label="Button" />
    <UButton color="neutral" variant="outline" icon="i-lucide-chevron-down" />
  </UFieldGroup>
</template>
```

### Size

Use the `size` prop to change the size of all the buttons.

```vue
<template>
  <UFieldGroup size="xl">
    <UButton color="neutral" variant="subtle" label="Button" />
    <UButton color="neutral" variant="outline" icon="i-lucide-chevron-down" />
  </UFieldGroup>
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the buttons. Defaults to `horizontal`.

```vue
<template>
  <UFieldGroup orientation="vertical">
    <UButton color="neutral" variant="subtle" label="Submit" />
    <UButton color="neutral" variant="outline" label="Cancel" />
  </UFieldGroup>
</template>
```

## Examples

### With input

You can use components like [Input](https://ui.nuxt.com/components/input), [InputMenu](https://ui.nuxt.com/components/input-menu), [Select](https://ui.nuxt.com/components/select) [SelectMenu](https://ui.nuxt.com/components/select-menu), etc. within a field group.

```vue
<template>
  <UFieldGroup>
    <UInput color="neutral" variant="outline" placeholder="Enter token" />

    <UButton color="neutral" variant="subtle" icon="i-lucide-clipboard" />
  </UFieldGroup>
</template>
```

### With tooltip

You can use a [Tooltip](https://ui.nuxt.com/components/tooltip) within a field group.

```vue [FieldGroupTooltipExample.vue]
<template>
  <UFieldGroup>
    <UInput color="neutral" variant="outline" placeholder="Enter token" />

    <UTooltip text="Copy to clipboard">
      <UButton
        color="neutral"
        variant="subtle"
        icon="i-lucide-clipboard"
      />
    </UTooltip>
  </UFieldGroup>
</template>
```

### With dropdown menu

You can use a [DropdownMenu](https://ui.nuxt.com/components/dropdown-menu) within a field group.

```vue [FieldGroupDropdownExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[] = [
  {
    label: 'Team',
    icon: 'i-lucide-users'
  },
  {
    label: 'Invite users',
    icon: 'i-lucide-user-plus',
    children: [
      {
        label: 'Invite by email',
        icon: 'i-lucide-send-horizontal'
      },
      {
        label: 'Invite by link',
        icon: 'i-lucide-link'
      }
    ]
  },
  {
    label: 'New team',
    icon: 'i-lucide-plus'
  }
]
</script>

<template>
  <UFieldGroup>
    <UButton color="neutral" variant="subtle" label="Settings" />

    <UDropdownMenu :items="items">
      <UButton
        color="neutral"
        variant="outline"
        icon="i-lucide-chevron-down"
      />
    </UDropdownMenu>
  </UFieldGroup>
</template>
```

### With badge

You can use a [Badge](https://ui.nuxt.com/components/badge) within a field group.

```vue [FieldGroupBadgeExample.vue]
<template>
  <UFieldGroup>
    <UBadge color="neutral" variant="outline" size="lg" label="https://" />

    <UInput color="neutral" variant="outline" placeholder="www.example.com" />
  </UFieldGroup>
</template>
```

## API

### Props

```ts
/**
 * Props for the FieldGroup component
 */
interface FieldGroupProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * The orientation the buttons are laid out.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the FieldGroup component
 */
interface FieldGroupSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    fieldGroup: {
      base: 'relative',
      variants: {
        size: {
          xs: '',
          sm: '',
          md: '',
          lg: '',
          xl: ''
        },
        orientation: {
          horizontal: 'inline-flex -space-x-px',
          vertical: 'flex flex-col -space-y-px'
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/FieldGroup.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/field-group.ts).


# FileUpload

## Usage

Use the `v-model` directive to control the value of the FileUpload.

```vue
<template>
  <UFileUpload class="w-96 min-h-48" />
</template>
```

### Multiple

Use the `multiple` prop to allow multiple files to be selected.

```vue
<template>
  <UFileUpload multiple class="w-96 min-h-48" />
</template>
```

### Dropzone

Use the `dropzone` prop to enable/disable the droppable area. Defaults to `true`.

```vue
<template>
  <UFileUpload :dropzone="false" class="w-96 min-h-48" />
</template>
```

### Interactive

Use the `interactive` prop to enable/disable the clickable area. Defaults to `true`.

\> \[!TIP]
\> See: #with-files-bottom-slot
\> This can be useful when adding a \`Button\` component in the \`#actions\` slot.

```vue
<template>
  <UFileUpload :interactive="false" class="w-96 min-h-48" />
</template>
```

### Accept

Use the `accept` prop to specify the allowed file types for the input. Provide a comma-separated list of [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types){rel="&#x22;nofollow&#x22;"} or file extensions (e.g., `image/png,application/pdf,.jpg`). Defaults to `*` (all file types).

```vue
<template>
  <UFileUpload accept="image/*" class="w-96 min-h-48" />
</template>
```

### Label

Use the `label` prop to set the label of the FileUpload.

```vue
<template>
  <UFileUpload label="Drop your image here" class="w-96 min-h-48" />
</template>
```

### Description

Use the `description` prop to set the description of the FileUpload.

```vue
<template>
  <UFileUpload label="Drop your image here" description="SVG, PNG, JPG or GIF (max. 2MB)" class="w-96 min-h-48" />
</template>
```

### Icon

Use the `icon` prop to set the icon of the FileUpload. Defaults to `i-lucide-upload`.

```vue
<template>
  <UFileUpload icon="i-lucide-image" label="Drop your image here" description="SVG, PNG, JPG or GIF (max. 2MB)" class="w-96 min-h-48" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.upload\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.upload\` key.

### Color

Use the `color` prop to change the color of the FileUpload.

```vue
<template>
  <UFileUpload color="neutral" highlight label="Drop your image here" description="SVG, PNG, JPG or GIF (max. 2MB)" class="w-96 min-h-48" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the FileUpload.

```vue
<template>
  <UFileUpload variant="button" />
</template>
```

### Size

Use the `size` prop to change the size of the FileUpload.

```vue
<template>
  <UFileUpload size="xl" variant="area" label="Drop your image here" description="SVG, PNG, JPG or GIF (max. 2MB)" />
</template>
```

### Layout

Use the `layout` prop to change how the files are displayed in the FileUpload. Defaults to `grid`.

\> \[!WARNING]
\> This prop only works when \`variant\` is \`area\`.

```vue
<template>
  <UFileUpload layout="list" multiple label="Drop your images here" description="SVG, PNG, JPG or GIF (max. 2MB)" class="w-96" />
</template>
```

### Position

Use the `position` prop to change the position of the files in the FileUpload. Defaults to `outside`.

\> \[!WARNING]
\> This prop only works when \`variant\` is \`area\` and when \`layout\` is \`list\`.

```vue
<template>
  <UFileUpload position="inside" layout="list" multiple label="Drop your images here" description="SVG, PNG, JPG or GIF (max. 2MB)" class="w-96" />
</template>
```

## Examples

### With Form validation

You can use the FileUpload within a [Form](https://ui.nuxt.com/docs/components/form) and [FormField](https://ui.nuxt.com/docs/components/form-field) components to handle validation and error handling.

```vue [FileUploadFormValidationExample.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']

const formatBytes = (bytes: number, decimals = 2) => {
  if (bytes === 0) return '0 Bytes'
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

const schema = z.object({
  image: z
    .instanceof(File, {
      message: 'Please select an image file.'
    })
    .refine(file => file.size <= MAX_FILE_SIZE, {
      message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
    })
    .refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
      message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
    })
    .refine(
      file =>
        new Promise((resolve) => {
          const reader = new FileReader()
          reader.onload = (e) => {
            const img = new Image()
            img.onload = () => {
              const meetsDimensions
                = img.width >= MIN_DIMENSIONS.width
                  && img.height >= MIN_DIMENSIONS.height
                  && img.width <= MAX_DIMENSIONS.width
                  && img.height <= MAX_DIMENSIONS.height
              resolve(meetsDimensions)
            }
            img.src = e.target?.result as string
          }
          reader.readAsDataURL(file)
        }),
      {
        message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
      }
    )
})

type Schema = z.output<typeof schema>

const state = reactive<Partial<Schema>>({
  image: undefined
})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4 w-96" @submit="onSubmit">
    <UFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
      <UFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
    </UFormField>

    <UButton type="submit" label="Submit" color="neutral" />
  </UForm>
</template>
```

### With default slot

You can use the default slot to make your own FileUpload component.

```vue [FileUploadDefaultSlotExample.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']

const formatBytes = (bytes: number, decimals = 2) => {
  if (bytes === 0) return '0 Bytes'
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

const schema = z.object({
  avatar: z
    .instanceof(File, {
      message: 'Please select an image file.'
    })
    .refine(file => file.size <= MAX_FILE_SIZE, {
      message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
    })
    .refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
      message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
    })
    .refine(
      file =>
        new Promise((resolve) => {
          const reader = new FileReader()
          reader.onload = (e) => {
            const img = new Image()
            img.onload = () => {
              const meetsDimensions
                = img.width >= MIN_DIMENSIONS.width
                  && img.height >= MIN_DIMENSIONS.height
                  && img.width <= MAX_DIMENSIONS.width
                  && img.height <= MAX_DIMENSIONS.height
              resolve(meetsDimensions)
            }
            img.src = e.target?.result as string
          }
          reader.readAsDataURL(file)
        }),
      {
        message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
      }
    )
})

type Schema = z.output<typeof schema>

const state = reactive<Partial<Schema>>({
  avatar: undefined
})

function createObjectUrl(file: File): string {
  return URL.createObjectURL(file)
}

async function onSubmit(event: FormSubmitEvent<Schema>) {
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4 w-64" @submit="onSubmit">
    <UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
      <UFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
        <div class="flex flex-wrap items-center gap-3">
          <UAvatar size="lg" :src="state.avatar ? createObjectUrl(state.avatar) : undefined" icon="i-lucide-image" />

          <UButton :label="state.avatar ? 'Change image' : 'Upload image'" color="neutral" variant="outline" @click="open()" />
        </div>

        <p v-if="state.avatar" class="text-xs text-muted mt-1.5">
          {{ state.avatar.name }}

          <UButton
            label="Remove"
            color="error"
            variant="link"
            size="xs"
            class="p-0"
            @click="removeFile()"
          />
        </p>
      </UFileUpload>
    </UFormField>

    <UButton type="submit" label="Submit" color="neutral" />
  </UForm>
</template>
```

### With files-bottom slot

You can use the `files-bottom` slot to add a [Button](https://ui.nuxt.com/docs/components/button) under the files list to remove all files for example.

```vue [FileUploadFilesBottomSlotExample.vue]
<script setup lang="ts">
const value = ref<File[]>([])
</script>

<template>
  <UFileUpload
    v-model="value"
    icon="i-lucide-image"
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    layout="list"
    multiple
    :interactive="false"
    class="w-96 min-h-48"
  >
    <template #actions="{ open }">
      <UButton
        label="Select images"
        icon="i-lucide-upload"
        color="neutral"
        variant="outline"
        @click="open()"
      />
    </template>

    <template #files-bottom="{ removeFile, files }">
      <UButton
        v-if="files?.length"
        label="Remove all files"
        color="neutral"
        @click="removeFile()"
      />
    </template>
  </UFileUpload>
</template>
```

\> \[!NOTE]
\> See: #interactive
\> The \`interactive\` prop is set to \`false\` in this example to prevent the default clickable area.

### With files-top slot

You can use the `files-top` slot to add a [Button](https://ui.nuxt.com/docs/components/button) above the files list to add new files for example.

```vue [FileUploadFilesTopSlotExample.vue]
<script setup lang="ts">
const value = ref<File[]>([])
</script>

<template>
  <UFileUpload
    v-model="value"
    icon="i-lucide-image"
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    layout="grid"
    multiple
    :interactive="false"
    class="w-96 min-h-48"
  >
    <template #actions="{ open }">
      <UButton
        label="Select images"
        icon="i-lucide-upload"
        color="neutral"
        variant="outline"
        @click="open()"
      />
    </template>

    <template #files-top="{ open, files }">
      <div v-if="files?.length" class="mb-2 flex items-center justify-between">
        <p class="font-bold">
          Files ({{ files?.length }})
        </p>

        <UButton
          icon="i-lucide-plus"
          label="Add more"
          color="neutral"
          variant="outline"
          class="-my-2"
          @click="open()"
        />
      </div>
    </template>
  </UFileUpload>
</template>
```

## API

### Props

```ts
/**
 * Props for the FileUpload component
 */
interface FileUploadProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  id?: string | undefined;
  name?: string | undefined;
  /**
   * The icon to display.
   */
  icon?: any;
  label?: string | undefined;
  description?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The `button` variant is only available when `multiple` is `false`.
   */
  variant?: "button" | "area" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * The layout of how files are displayed.
   * Only works when `variant` is `area`.
   * @default "\"grid\""
   */
  layout?: "list" | "grid" | undefined;
  /**
   * The position of the files.
   * Only works when `variant` is `area` and when `layout` is `list`.
   * @default "\"outside\""
   */
  position?: "inside" | "outside" | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Specifies the allowed file types for the input. Provide a comma-separated list of MIME types or file extensions (e.g., "image/png,application/pdf,.jpg").
   * @default "\"*\""
   */
  accept?: string | undefined;
  /**
   * @default "false as never"
   */
  multiple?: M | undefined;
  /**
   * Reset the file input when the dialog is opened.
   * @default "false"
   */
  reset?: boolean | undefined;
  /**
   * Create a zone that allows the user to drop files onto it.
   * @default "true"
   */
  dropzone?: boolean | undefined;
  /**
   * Make the dropzone interactive when the user is clicking on it.
   * @default "true"
   */
  interactive?: boolean | undefined;
  required?: boolean | undefined;
  disabled?: boolean | undefined;
  /**
   * The icon to display for the file.
   */
  fileIcon?: any;
  /**
   * Preview the file (currently only `<img>` is rendered)
   * When set false, only `fileIcon` is displayed
   * @default "true"
   */
  fileImage?: boolean | undefined;
  /**
   * Configure the delete button for the file.
   * When `layout` is `grid`, the default is `{ color: 'neutral', variant: 'solid', size: 'xs' }`{lang="ts-type"}
   * When `layout` is `list`, the default is `{ color: 'neutral', variant: 'link' }`{lang="ts-type"}
   * @default "true"
   */
  fileDelete?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed to delete a file.
   */
  fileDeleteIcon?: any;
  /**
   * Show the file preview/list after upload.
   * @default "true"
   */
  preview?: boolean | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; wrapper?: ClassNameValue; icon?: ClassNameValue; avatar?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; actions?: ClassNameValue; files?: ClassNameValue; file?: ClassNameValue; fileLeadingAvatar?: ClassNameValue; fileWrapper?: ClassNameValue; fileName?: ClassNameValue; fileSize?: ClassNameValue; fileTrailingButton?: ClassNameValue; } | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  modelValue?: (M extends true ? File[] : File) | null | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
\> This component also supports all native \`\<input>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the FileUpload component
 */
interface FileUploadSlots {
  default(): any;
  leading(): any;
  label(): any;
  description(): any;
  actions(): any;
  files(): any;
  files-top(): any;
  files-bottom(): any;
  file(): any;
  file-leading(): any;
  file-name(): any;
  file-size(): any;
  file-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the FileUpload component
 */
interface FileUploadEmits {
  change: (payload: [event: Event]) => void;
  update:modelValue: (payload: [value: (M extends true ? File[] : File) | null | undefined]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                               |
| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    | `Ref<HTMLInputElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `dropzoneRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLDivElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}   |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    fileUpload: {
      slots: {
        root: 'relative flex flex-col',
        base: [
          'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-2',
          'transition-[background]'
        ],
        wrapper: 'flex flex-col items-center justify-center text-center',
        icon: 'shrink-0',
        avatar: 'shrink-0',
        label: 'font-medium text-default mt-2',
        description: 'text-muted mt-1',
        actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
        files: '',
        file: 'relative',
        fileLeadingAvatar: 'shrink-0',
        fileWrapper: 'flex flex-col min-w-0',
        fileName: 'text-default truncate',
        fileSize: 'text-muted truncate',
        fileTrailingButton: ''
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          area: {
            wrapper: 'px-4 py-3',
            base: 'p-4'
          },
          button: {}
        },
        size: {
          xs: {
            base: 'text-xs',
            icon: 'size-4',
            file: 'text-xs px-2 py-1 gap-1',
            fileWrapper: 'flex-row gap-1'
          },
          sm: {
            base: 'text-xs',
            icon: 'size-4',
            file: 'text-xs px-2.5 py-1.5 gap-1.5',
            fileWrapper: 'flex-row gap-1'
          },
          md: {
            base: 'text-sm',
            icon: 'size-5',
            file: 'text-xs px-2.5 py-1.5 gap-1.5'
          },
          lg: {
            base: 'text-sm',
            icon: 'size-5',
            file: 'text-sm px-3 py-2 gap-2',
            fileSize: 'text-xs'
          },
          xl: {
            base: 'text-base',
            icon: 'size-6',
            file: 'text-sm px-3 py-2 gap-2'
          }
        },
        layout: {
          list: {
            root: 'gap-2 items-start',
            files: 'flex flex-col w-full gap-2',
            file: 'min-w-0 flex items-center border border-default rounded-md w-full',
            fileTrailingButton: 'ms-auto'
          },
          grid: {
            fileWrapper: 'hidden',
            fileLeadingAvatar: 'size-full rounded-lg',
            fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
          }
        },
        position: {
          inside: '',
          outside: ''
        },
        dropzone: {
          true: 'border-dashed data-[dragging=true]:bg-elevated/25'
        },
        interactive: {
          true: ''
        },
        highlight: {
          true: ''
        },
        multiple: {
          true: ''
        },
        disabled: {
          true: 'cursor-not-allowed opacity-75'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          class: 'focus-visible:outline-primary'
        },
        {
          color: 'secondary',
          class: 'focus-visible:outline-secondary'
        },
        {
          color: 'success',
          class: 'focus-visible:outline-success'
        },
        {
          color: 'info',
          class: 'focus-visible:outline-info'
        },
        {
          color: 'warning',
          class: 'focus-visible:outline-warning'
        },
        {
          color: 'error',
          class: 'focus-visible:outline-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'border-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'border-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'border-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'border-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'border-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'border-error'
        },
        {
          color: 'neutral',
          class: 'focus-visible:outline-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'border-inverted'
        },
        {
          size: 'xs',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1'
          }
        },
        {
          size: 'sm',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1.5'
          }
        },
        {
          size: 'md',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1.5'
          }
        },
        {
          size: 'lg',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-2'
          }
        },
        {
          size: 'xl',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-2'
          }
        },
        {
          variant: 'button',
          size: 'xs',
          class: {
            base: 'p-1'
          }
        },
        {
          variant: 'button',
          size: 'sm',
          class: {
            base: 'p-1.5'
          }
        },
        {
          variant: 'button',
          size: 'md',
          class: {
            base: 'p-1.5'
          }
        },
        {
          variant: 'button',
          size: 'lg',
          class: {
            base: 'p-2'
          }
        },
        {
          variant: 'button',
          size: 'xl',
          class: {
            base: 'p-2'
          }
        },
        {
          layout: 'grid',
          multiple: true,
          class: {
            files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
            file: 'p-0 aspect-square'
          }
        },
        {
          layout: 'grid',
          multiple: false,
          class: {
            file: 'absolute inset-0 p-0'
          }
        },
        {
          interactive: true,
          disabled: false,
          class: 'hover:bg-elevated/25'
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'area',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/FileUpload.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/file-upload.ts).


# Footer

## Usage

The Footer component renders a `<footer>` element.

Use the `left`, `default` and `right` slots to customize the footer.

```vue [FooterExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Figma Kit',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Playground',
  to: 'https://stackblitz.com/edit/nuxt-ui',
  target: '_blank'
}, {
  label: 'Releases',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}]
</script>

<template>
  <UFooter>
    <template #left>
      <p class="text-muted text-sm">
        Copyright © {{ new Date().getFullYear() }}
      </p>
    </template>

    <UNavigationMenu :items="items" variant="link" />

    <template #right>
      <UButton
        icon="i-simple-icons-discord"
        color="neutral"
        variant="ghost"
        to="https://go.nuxt.com/discord"
        target="_blank"
        aria-label="Discord"
      />
      <UButton
        icon="i-simple-icons-x"
        color="neutral"
        variant="ghost"
        to="https://go.nuxt.com/x"
        target="_blank"
        aria-label="X"
      />
      <UButton
        icon="i-simple-icons-github"
        color="neutral"
        variant="ghost"
        to="https://github.com/nuxt/nuxt"
        target="_blank"
        aria-label="GitHub"
      />
    </template>
  </UFooter>
</template>
```

\> \[!NOTE]
\> In this example, we use the \[NavigationMenu]\(/docs/components/navigation-menu) component to render the footer links in the center.

\> \[!TIP]
\> See: /docs/components/footer-columns
\> You can use the \`FooterColumns\` component to display a list of links inside the \`top\` slot.

## Examples

### Within `app.vue`

Use the Footer component in your `app.vue` or in a layout:

```vue [app.vue] {32-67}
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [{
  label: 'Figma Kit',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Playground',
  to: 'https://stackblitz.com/edit/nuxt-ui',
  target: '_blank'
}, {
  label: 'Releases',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}]
</script>

<template>
  <UApp>
    <UHeader />

    <UMain>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </UMain>

    <USeparator icon="i-simple-icons-nuxtdotjs" type="dashed" class="h-px" />

    <UFooter>
      <template #left>
        <p class="text-muted text-sm">
          Copyright © {{ new Date().getFullYear() }}
        </p>
      </template>

      <UNavigationMenu :items="items" variant="link" />

      <template #right>
        <UButton
          icon="i-simple-icons-discord"
          color="neutral"
          variant="ghost"
          to="https://go.nuxt.com/discord"
          target="_blank"
          aria-label="Discord"
        />
        <UButton
          icon="i-simple-icons-x"
          color="neutral"
          variant="ghost"
          to="https://go.nuxt.com/x"
          target="_blank"
          aria-label="X"
        />
        <UButton
          icon="i-simple-icons-github"
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/nuxt"
          target="_blank"
          aria-label="GitHub"
        />
      </template>
    </UFooter>
  </UApp>
</template>
```

\> \[!NOTE]
\> In this example, we use the \[Separator]\(/docs/components/separator) component to add a border above the footer.

## API

### Props

```ts
/**
 * Props for the Footer component
 */
interface FooterProps {
  /**
   * The element or component this component should render as.
   * @default "\"footer\""
   */
  as?: any;
  ui?: { root?: ClassNameValue; top?: ClassNameValue; bottom?: ClassNameValue; container?: ClassNameValue; left?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Footer component
 */
interface FooterSlots {
  left(): any;
  default(): any;
  right(): any;
  top(): any;
  bottom(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    footer: {
      slots: {
        root: '',
        top: 'py-8 lg:py-12',
        bottom: 'py-8 lg:py-12',
        container: 'py-8 lg:py-4 lg:flex lg:items-center lg:justify-between lg:gap-x-3',
        left: 'flex items-center justify-center lg:justify-start lg:flex-1 gap-x-1.5 mt-3 lg:mt-0 lg:order-1',
        center: 'mt-3 lg:mt-0 lg:order-2 flex items-center justify-center',
        right: 'lg:flex-1 flex items-center justify-center lg:justify-end gap-x-1.5 lg:order-3'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Footer.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/footer.ts).


# FooterColumns

## Usage

The FooterColumns component renders a list of columns to display in your Footer.

Use it in the `top` slot of the [Footer](https://ui.nuxt.com/docs/components/footer) component:

```vue {3-7}
<template>
  <UFooter>
    <template #top>
      <UContainer>
        <UFooterColumns />
      </UContainer>
    </template>
  </UFooter>
</template>
```

### Columns

Use the `columns` prop as an array of objects with the following properties:

- `label: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `children?: FooterColumnLink[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Each column contains a `children` array of objects that define the links. Each link can have the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkLeadingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue [FooterColumnsExample.vue]
<script setup lang="ts">
import type { FooterColumn } from '@nuxt/ui'

const columns: FooterColumn[] = [{
  label: 'Community',
  children: [{
    label: 'Nuxters',
    to: 'https://nuxters.nuxt.com',
    target: '_blank'
  }, {
    label: 'Video Courses',
    to: 'https://masteringnuxt.com/nuxt3?ref=nuxt',
    target: '_blank'
  }, {
    label: 'Nuxt on GitHub',
    to: 'https://github.com/nuxt',
    target: '_blank'
  }]
}, {
  label: 'Solutions',
  children: [{
    label: 'Nuxt Content',
    to: 'https://content.nuxt.com/',
    target: '_blank'
  }, {
    label: 'Nuxt DevTools',
    to: 'https://devtools.nuxt.com/',
    target: '_blank'
  }, {
    label: 'Nuxt Image',
    to: 'https://image.nuxt.com/',
    target: '_blank'
  }, {
    label: 'Nuxt UI',
    to: 'https://ui.nuxt.com/',
    target: '_blank'
  }]
}]
</script>

<template>
  <UFooterColumns :columns="columns">
    <template #right>
      <UFormField name="email" label="Subscribe to our newsletter" size="lg">
        <UInput type="email" class="w-full">
          <template #trailing>
            <UButton type="submit" size="xs" color="neutral" label="Subscribe" />
          </template>
        </UInput>
      </UFormField>
    </template>
  </UFooterColumns>
</template>
```

## API

### Props

```ts
/**
 * Props for the FooterColumns component
 */
interface FooterColumnsProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  columns?: FooterColumn<T>[] | undefined;
  ui?: { root?: ClassNameValue; left?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; label?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkLabelExternalIcon?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the FooterColumns component
 */
interface FooterColumnsSlots {
  left(): any;
  default(): any;
  right(): any;
  column-label(): any;
  link(): any;
  link-leading(): any;
  link-label(): any;
  link-trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    footerColumns: {
      slots: {
        root: 'xl:grid xl:grid-cols-3 xl:gap-8',
        left: 'mb-10 xl:mb-0',
        center: 'flex flex-col lg:grid grid-flow-col auto-cols-fr gap-8 xl:col-span-2',
        right: 'mt-10 xl:mt-0',
        label: 'text-sm font-semibold',
        list: 'mt-6 space-y-4',
        item: 'relative',
        link: 'group text-sm flex items-center gap-1.5 focus-visible:outline-primary',
        linkLeadingIcon: 'size-5 shrink-0',
        linkLabel: 'truncate',
        linkLabelExternalIcon: 'size-3 absolute top-0 text-dimmed inline-block'
      },
      variants: {
        active: {
          true: {
            link: 'text-primary font-medium'
          },
          false: {
            link: [
              'text-muted hover:text-default',
              'transition-colors'
            ]
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/FooterColumns.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/footer-columns.ts).


# Form

## Usage

Use the Form component to validate form data using any validation library supporting [Standard Schema](https://github.com/standard-schema/standard-schema){rel="&#x22;nofollow&#x22;"} such as [Valibot](https://github.com/fabian-hiller/valibot){rel="&#x22;nofollow&#x22;"}, [Zod](https://github.com/colinhacks/zod){rel="&#x22;nofollow&#x22;"}, [Regle](https://github.com/victorgarciaesgi/regle){rel="&#x22;nofollow&#x22;"}, [Yup](https://github.com/jquense/yup){rel="&#x22;nofollow&#x22;"}, [Joi](https://github.com/hapijs/joi){rel="&#x22;nofollow&#x22;"} or [Superstruct](https://github.com/ianstormtaylor/superstruct){rel="&#x22;nofollow&#x22;"} or your own validation logic.

It works with the [FormField](https://ui.nuxt.com/docs/components/form-field) component to display error messages around form elements automatically.

### Schema validation

It requires two props:

- `state` - a reactive object holding the form's state.
- `schema` - any [Standard Schema](https://github.com/standard-schema/standard-schema){rel="&#x22;nofollow&#x22;"} or [Superstruct](https://github.com/ianstormtaylor/superstruct){rel="&#x22;nofollow&#x22;"}.

\> \[!WARNING]
\> No validation library is included by default, ensure you install the one you need.

\`\`\`vue
\<script setup lang="ts">
import \* as v from 'valibot'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters'))
})
type Schema = v.InferOutput\<typeof schema>
const state = reactive({
email: '',
password: ''
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent\<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="schema" \:state="state" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="state.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="state.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`
\`\`\`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('Password is required').min(8, 'Must be at least 8 characters')
})
type Schema = z.output\<typeof schema>
const state = reactive\<Partial\<Schema>>({
email: undefined,
password: undefined
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent\<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="schema" \:state="state" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="state.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="state.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
import { useRegle, type InferInput } from '@regle/core'
import { required, email, minLength, withMessage } from '@regle/rules'
import type { FormSubmitEvent } from '@nuxt/ui'
const { r$ } = useRegle({ email: '', password: '' }, {
email: { required, email: withMessage(email, 'Invalid email') },
password: { required, minLength: withMessage(minLength(8), 'Must be at least 8 characters') }
})
type Schema = InferInput\<typeof r$>
const toast = useToast()
async function onSubmit(event: FormSubmitEvent\<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="r$" \:state="r$.$value" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="r$.$value.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="r$.$value.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
import { object, string } from 'yup'
import type { InferType } from 'yup'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({
email: string().email('Invalid email').required('Required'),
password: string()
.min(8, 'Must be at least 8 characters')
.required('Required')
})
type Schema = InferType\<typeof schema>
const state = reactive({
email: undefined,
password: undefined
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent\<Schema>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="schema" \:state="state" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="state.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="state.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
import Joi from 'joi'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = Joi.object({
email: Joi.string().required(),
password: Joi.string()
.min(8)
.required()
})
const state = reactive({
email: undefined,
password: undefined
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent\<typeof state>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="schema" \:state="state" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="state.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="state.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`
\`\`\`vue
\<script setup lang="ts">
import { object, string, nonempty, refine } from 'superstruct'
import type { Infer } from 'superstruct'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({
email: nonempty(string()),
password: refine(string(), 'Password', (value) => {
if (value.length >= 8) return true
return 'Must be at least 8 characters'
})
})
const state = reactive({
email: '',
password: ''
})
type Schema = Infer\<typeof schema>
async function onSubmit(event: FormSubmitEvent\<Schema>) {
console.log(event.data)
}
\</script>
\<template>
\<UForm \:schema="schema" \:state="state" class="space-y-4" @submit="onSubmit">
\<UFormField label="Email" name="email">
\<UInput v-model="state.email" />
\</UFormField>
\<UFormField label="Password" name="password">
\<UInput v-model="state.password" type="password" />
\</UFormField>
\<UButton type="submit">
Submit
\</UButton>
\</UForm>
\</template>
\`\`\`

Errors are reported directly to the [FormField](https://ui.nuxt.com/docs/components/form-field) component based on the `name` or `error-pattern` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="vue"}.

Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts"} will be applied to `<FormField name="user.email">`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="vue"}.

### Custom validation

Use the `validate` prop to apply your own validation logic.

The validation function must return a list of errors with the following attributes:

- `message` - the error message to display.
- `name` - the `name` of the `FormField` to send the error to.

\> \[!TIP]
\> It can be used alongside the \`schema\` prop to handle complex use cases.

```vue [FormExampleBasic.vue]
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '@nuxt/ui'

const state = reactive({
  email: undefined,
  password: undefined
})

type Schema = typeof state

function validate(state: Partial<Schema>): FormError[] {
  const errors = []
  if (!state.email) errors.push({ name: 'email', message: 'Required' })
  if (!state.password) errors.push({ name: 'password', message: 'Required' })
  return errors
}

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormField label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormField>

    <UFormField label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
```

### Input events

The Form component automatically triggers validation when an input emits an `input`, `change`, or `blur` event.

- Validation on `input` occurs **as you type**.
- Validation on `change` occurs when you **commit to a value**.
- Validation on `blur` happens when an input **loses focus**.

You can control when validation happens this using the `validate-on` prop.

\> \[!TIP]
\> The form always validates on submit.

```vue [FormExampleElements.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  input: z.string().min(10),
  inputNumber: z.number().min(10),
  inputMenu: z.any().refine(option => option?.value === 'option-2', {
    message: 'Select Option 2'
  }),
  inputMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
    message: 'Include Option 2'
  }),
  textarea: z.string().min(10),
  select: z.string().refine(value => value === 'option-2', {
    message: 'Select Option 2'
  }),
  selectMultiple: z.array(z.string()).refine(values => values.includes('option-2'), {
    message: 'Include Option 2'
  }),
  selectMenu: z.any().refine(option => option?.value === 'option-2', {
    message: 'Select Option 2'
  }),
  selectMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
    message: 'Include Option 2'
  }),
  switch: z.boolean().refine(value => value === true, {
    message: 'Toggle me'
  }),
  checkbox: z.boolean().refine(value => value === true, {
    message: 'Check me'
  }),
  radioGroup: z.string().refine(value => value === 'option-2', {
    message: 'Select Option 2'
  }),
  checkboxGroup: z.any().refine(values => !!values?.find((option: any) => option === 'option-2'), {
    message: 'Include Option 2'
  }),
  slider: z.number().max(20, { message: 'Must be less than 20' }),
  pin: z.string().regex(/^\d$/).array().length(5),
  file: z.file().min(1).max(1024 * 1024).mime('image/png')
})

type Schema = z.input<typeof schema>

const state = reactive<Partial<Schema>>({})

const form = useTemplateRef('form')

const items = [
  { label: 'Option 1', value: 'option-1' },
  { label: 'Option 2', value: 'option-2' },
  { label: 'Option 3', value: 'option-3' }
]

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm ref="form" :state="state" :schema="schema" class="w-full" @submit="onSubmit">
    <div class="grid grid-cols-3 gap-4">
      <UFormField label="Input" name="input">
        <UInput v-model="state.input" placeholder="john@lennon.com" class="w-full" />
      </UFormField>

      <div class="flex flex-col gap-4">
        <UFormField name="switch">
          <USwitch v-model="state.switch" label="Switch me" />
        </UFormField>

        <UFormField name="checkbox">
          <UCheckbox v-model="state.checkbox" label="Check me" />
        </UFormField>
      </div>

      <UFormField name="slider" label="Slider">
        <USlider v-model="state.slider" />
      </UFormField>

      <UFormField name="select" label="Select">
        <USelect v-model="state.select" :items="items" class="w-full" />
      </UFormField>

      <UFormField name="selectMultiple" label="Select (Multiple)">
        <USelect v-model="state.selectMultiple" multiple :items="items" class="w-full" />
      </UFormField>

      <UFormField name="selectMenu" label="Select Menu">
        <USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
      </UFormField>

      <UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
        <USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-full" />
      </UFormField>

      <UFormField name="inputMenu" label="Input Menu">
        <UInputMenu v-model="state.inputMenu" :items="items" class="w-full" />
      </UFormField>

      <UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
        <UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" class="w-full" />
      </UFormField>

      <UFormField name="inputNumber" label="Input Number">
        <UInputNumber v-model="state.inputNumber" class="w-full" />
      </UFormField>

      <UFormField label="Textarea" name="textarea">
        <UTextarea v-model="state.textarea" class="w-full" />
      </UFormField>
      <div class="flex gap-4">
        <UFormField name="radioGroup">
          <URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" />
        </UFormField>
        <UFormField name="checkboxGroup">
          <UCheckboxGroup v-model="state.checkboxGroup" legend="Checkbox group" :items="items" />
        </UFormField>
      </div>
      <UFormField name="pin" label="Pin Input" :error-pattern="/(pin)\..*/">
        <UPinInput v-model="state.pin" />
      </UFormField>

      <UFormField name="file" label="File Input">
        <UFileUpload
          v-model="state.file"
          label="Drop your image here"
          description="PNG (max. 1MB)"
          class="w-full min-h-44"
        />
      </UFormField>
    </div>

    <div class="flex gap-2 mt-8">
      <UButton type="submit">
        Submit
      </UButton>

      <UButton variant="outline" @click="form?.clear()">
        Clear
      </UButton>
    </div>
  </UForm>
</template>
```

\> \[!TIP]
\> You can use the \[\`useFormField\`]\(/docs/composables/use-form-field) composable to implement this inside your own components.

### Error event

You can listen to the `@error` event to handle errors. This event is triggered when the form is submitted and contains an array of `FormError` objects with the following fields:

- `id` - the input's `id`.
- `name` - the `name` of the `FormField`
- `message` - the error message to display.

Here's an example that focuses the first input element with an error after the form is submitted:

```vue [FormExampleOnError.vue]
<script setup lang="ts">
import type { FormError, FormErrorEvent, FormSubmitEvent } from '@nuxt/ui'

const state = reactive({
  email: undefined,
  password: undefined
})

type Schema = typeof state

function validate(state: Partial<Schema>): FormError[] {
  const errors = []
  if (!state.email) errors.push({ name: 'email', message: 'Required' })
  if (!state.password) errors.push({ name: 'password', message: 'Required' })
  return errors
}

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}

async function onError(event: FormErrorEvent) {
  if (event?.errors?.[0]?.id) {
    const element = document.getElementById(event.errors[0].id)
    element?.focus()
    element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit" @error="onError">
    <UFormField label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormField>

    <UFormField label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
```

### HTML5 validation `4.5+`

When calling `form.submit()` programmatically, the Form component automatically triggers native HTML5 validation before submission.

\> \[!NOTE]
\> This is particularly useful when the submit button is outside the form element, such as in a modal footer.

```vue [FormExampleHtml5Validation.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'

const state = reactive({
  email: undefined,
  age: undefined
})

type Schema = typeof state

const form = useTemplateRef('form')

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <div class="space-y-4">
    <UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
      <UFormField label="Email" name="email">
        <UInput v-model="state.email" type="email" required />
      </UFormField>

      <UFormField label="Age" name="age">
        <UInput v-model="state.age" type="number" min="18" max="100" required />
      </UFormField>
    </UForm>

    <UButton @click="form?.submit()">
      Submit
    </UButton>
  </div>
</template>
```

### Nesting forms

Use the `nested` prop to nest multiple Form components and link their validation functions. In this case, validating the parent form will automatically validate all the other forms inside it.

Nested forms directly inherit their parent's state, so you don't need to define a separate state for them. You can use the `name` prop to target a nested attribute within the parent's state.

It can be used to dynamically add fields based on user's input:

```vue [FormExampleNested.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  name: z.string().min(2),
  news: z.boolean().default(false)
})

type Schema = z.output<typeof schema>

const nestedSchema = z.object({
  email: z.email()
})

type NestedSchema = z.output<typeof nestedSchema>

const state = reactive<Partial<Schema & NestedSchema>>({ })

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm
    ref="form"
    :state="state"
    :schema="schema"
    class="gap-4 flex flex-col w-60"
    @submit="onSubmit"
  >
    <UFormField label="Name" name="name">
      <UInput v-model="state.name" placeholder="John Lennon" />
    </UFormField>

    <div>
      <UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" />
    </div>

    <UForm v-if="state.news" :schema="nestedSchema" nested>
      <UFormField label="Email" name="email">
        <UInput v-model="state.email" placeholder="john@lennon.com" />
      </UFormField>
    </UForm>

    <div>
      <UButton type="submit">
        Submit
      </UButton>
    </div>
  </UForm>
</template>
```

Or to validate list inputs:

```vue [FormExampleNestedList.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  customer: z.string().min(2)
})

type Schema = z.output<typeof schema>

const itemSchema = z.object({
  description: z.string().min(1),
  price: z.number().min(0)
})

type ItemSchema = z.output<typeof itemSchema>

const state = reactive<Partial<Schema & { items: Partial<ItemSchema>[] }>>({ })

function addItem() {
  if (!state.items) {
    state.items = []
  }
  state.items.push({})
}

function removeItem() {
  if (state.items) {
    state.items.pop()
  }
}

const toast = useToast()

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm
    :state="state"
    :schema="schema"
    class="gap-4 flex flex-col w-60"
    @submit="onSubmit"
  >
    <UFormField label="Customer" name="customer">
      <UInput v-model="state.customer" placeholder="Wonka Industries" />
    </UFormField>

    <UForm
      v-for="item, count in state.items"
      :key="count"
      :name="`items.${count}`"
      :schema="itemSchema"
      class="flex gap-2"
      nested
    >
      <UFormField :label="!count ? 'Description' : undefined" name="description">
        <UInput v-model="item.description" />
      </UFormField>
      <UFormField :label="!count ? 'Price' : undefined" name="price" class="w-20">
        <UInput v-model="item.price" type="number" />
      </UFormField>
    </UForm>

    <div class="flex gap-2">
      <UButton color="neutral" variant="subtle" size="sm" @click="addItem()">
        Add Item
      </UButton>

      <UButton color="neutral" variant="ghost" size="sm" @click="removeItem()">
        Remove Item
      </UButton>
    </div>
    <div>
      <UButton type="submit">
        Submit
      </UButton>
    </div>
  </UForm>
</template>
```

## API

### Props

```ts
/**
 * Props for the Form component
 */
interface FormProps {
  id?: string | number | undefined;
  /**
   * Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs.
   */
  schema?: S | undefined;
  /**
   * An object representing the current state of the form.
   */
  state?: (N extends false ? Partial<InferInput<S>> : never) | undefined;
  /**
   * Custom validation function to validate the form state.
   */
  validate?: ((state: Partial<InferInput<S>>) => FormError<string>[] | Promise<FormError<string>[]>) | undefined;
  /**
   * The list of input events that trigger the form validation.
   */
  validateOn?: FormInputEvents[] | undefined;
  /**
   * Disable all inputs inside the form.
   */
  disabled?: boolean | undefined;
  /**
   * Path of the form's state within it's parent form.
   * Used for nesting forms. Only available if `nested` is true.
   */
  name?: (N extends true ? string : never) | undefined;
  /**
   * Delay in milliseconds before validating the form on input events.
   * @default "300"
   */
  validateOnInputDelay?: number | undefined;
  /**
   * If true, applies schema transformations on submit.
   * @default "true as T"
   */
  transform?: T | undefined;
  /**
   * If true, this form will attach to its parent Form and validate at the same time.
   */
  nested?: N | undefined;
  /**
   * When `true`, all form elements will be disabled on `@submit` event.
   * This will cause any focused input elements to lose their focus state.
   * @default "true"
   */
  loadingAuto?: boolean | undefined;
  ui?: { base?: any; } | undefined;
  acceptcharset?: string | undefined;
  action?: string | undefined;
  autocomplete?: string | undefined;
  enctype?: string | undefined;
  method?: string | undefined;
  novalidate?: Booleanish | undefined;
  target?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attributes
\> This component also supports all native \`\<form>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Form component
 */
interface FormSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Form component
 */
interface FormEmits {
  submit: (payload: [event: FormSubmitEvent<FormData<S, T>>]) => void;
  error: (payload: [event: FormErrorEvent]) => void;
}
```

### Expose

You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
const form = useTemplateRef('form')
</script>

<template>
  <UForm ref="form" />
</template>
```

This will give you access to the following:

| Name                                                                                                                                                                                                                            | Type                                                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit()`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                                  | `Promise<void>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} :br ::div
---
className:
  - text-toned
  - mt-1
---
Triggers form submission with HTML5 validation.
::                                                                   |
| `validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean })`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Promise<T>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} :br ::div
---
className:
  - text-toned
  - mt-1
---
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
::                                 |
| `clear(path?: keyof T | RegExp)`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                            | `void` :br ::div
---
className:
  - text-toned
  - mt-1
---
Clears form errors associated with a specific path. If no path is provided, clears all form errors.
::                                                                                                                                            |
| `getErrors(path?: keyof T | RegExp)`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                        | `FormError[]`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} :br ::div
---
className:
  - text-toned
  - mt-1
---
Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.
::             |
| `setErrors(errors: FormError[], name?: keyof T | RegExp)`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                   | `void` :br ::div
---
className:
  - text-toned
  - mt-1
---
Sets form errors for a given path. If no path is provided, overrides all errors.
::                                                                                                                                                               |
| `errors`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                                    | `Ref<FormError[]>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} :br ::div
---
className:
  - text-toned
  - mt-1
---
A reference to the array containing validation errors. Use this to access or manipulate the error information.
:: |
| `disabled`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                                  | `Ref<boolean>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                                                                                                            |
| `dirty`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                                     | `Ref<boolean>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} `true` if at least one form field has been updated by the user.                                                                                                            |
| `dirtyFields`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                               | `DeepReadonly<Set<keyof T>>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} Tracks fields that have been modified by the user.                                                                                                           |
| `touchedFields`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                             | `DeepReadonly<Set<keyof T>>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} Tracks fields that the user interacted with.                                                                                                                 |
| `blurredFields`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                                             | `DeepReadonly<Set<keyof T>>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} Tracks fields blurred by the user.                                                                                                                           |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    form: {
      base: ''
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Form.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/form.ts).


# FormField

## Usage

Wrap any form component with a FormField. Used in a [Form](https://ui.nuxt.com/docs/components/form), it provides validation and error handling.

### Label

Use the `label` prop to set the label for the form control.

```vue
<template>
  <UFormField label="Email">
    <UInput placeholder="Enter your email" />
  </UFormField>
</template>
```

\> \[!NOTE]
\> The label \`for\` attribute and the form control are associated with a unique \`id\` if not provided.

When using the `required` prop, an asterisk is added next to the label.

```vue
<template>
  <UFormField label="Email" required>
    <UInput placeholder="Enter your email" />
  </UFormField>
</template>
```

### Description

Use the `description` prop to provide additional information below the label.

```vue
<template>
  <UFormField label="Email" description="We'll never share your email with anyone else.">
    <UInput placeholder="Enter your email" class="w-full" />
  </UFormField>
</template>
```

### Hint

Use the `hint` prop to display a hint message next to the label.

```vue
<template>
  <UFormField label="Email" hint="Optional">
    <UInput placeholder="Enter your email" />
  </UFormField>
</template>
```

### Help

Use the `help` prop to display a help message below the form control.

```vue
<template>
  <UFormField label="Email" help="Please enter a valid email address.">
    <UInput placeholder="Enter your email" class="w-full" />
  </UFormField>
</template>
```

### Error

Use the `error` prop to display an error message below the form control. When used together with the `help` prop, the `error` prop takes precedence.

When used inside a [Form](https://ui.nuxt.com/docs/components/form), this is automatically set when a validation error occurs.

```vue
<template>
  <UFormField label="Email" error="Please enter a valid email address.">
    <UInput placeholder="Enter your email" class="w-full" />
  </UFormField>
</template>
```

\> \[!TIP]
\> See: /docs/getting-started/theme/design-system#colors
\> This sets the \`color\` to \`error\` on the form control. You can change it globally in your \`app.config.ts\`.

### Size

Use the `size` prop to change the size of the FormField, the `size` is proxied to the form control.

```vue
<template>
  <UFormField label="Email" description="We'll never share your email with anyone else." hint="Optional" help="Please enter a valid email address." size="xl">
    <UInput placeholder="Enter your email" class="w-full" />
  </UFormField>
</template>
```

### Orientation `4.3+`

Use the `orientation` prop to change the layout of the FormField. Defaults to `vertical`.

```vue
<template>
  <UFormField orientation="horizontal" label="Email" help="Please enter a valid email address." class="w-72">
    <UInput placeholder="Enter your email" class="w-full" />
  </UFormField>
</template>
```

## API

### Props

```ts
/**
 * Props for the FormField component
 */
interface FormFieldProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The name of the FormField. Also used to match form errors.
   */
  name?: string | undefined;
  /**
   * A regular expression to match form error names.
   */
  errorPattern?: RegExp | undefined;
  label?: string | undefined;
  description?: string | undefined;
  help?: string | undefined;
  /**
   * @default "undefined"
   */
  error?: string | boolean | undefined;
  hint?: string | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  required?: boolean | undefined;
  /**
   * If true, validation on input will be active immediately instead of waiting for a blur event.
   */
  eagerValidation?: boolean | undefined;
  /**
   * Delay in milliseconds before validating the form on input events.
   */
  validateOnInputDelay?: number | undefined;
  /**
   * The orientation of the form field.
   */
  orientation?: "vertical" | "horizontal" | undefined;
  ui?: { root?: ClassNameValue; wrapper?: ClassNameValue; labelWrapper?: ClassNameValue; label?: ClassNameValue; container?: ClassNameValue; description?: ClassNameValue; error?: ClassNameValue; hint?: ClassNameValue; help?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the FormField component
 */
interface FormFieldSlots {
  label(): any;
  hint(): any;
  description(): any;
  help(): any;
  error(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    formField: {
      slots: {
        root: '',
        wrapper: '',
        labelWrapper: 'flex content-center items-center justify-between gap-1',
        label: 'block font-medium text-default',
        container: 'relative',
        description: 'text-muted',
        error: 'mt-2 text-error',
        hint: 'text-muted',
        help: 'mt-2 text-muted'
      },
      variants: {
        size: {
          xs: {
            root: 'text-xs'
          },
          sm: {
            root: 'text-xs'
          },
          md: {
            root: 'text-sm'
          },
          lg: {
            root: 'text-sm'
          },
          xl: {
            root: 'text-base'
          }
        },
        required: {
          true: {
            label: "after:content-['*'] after:ms-0.5 after:text-error"
          }
        },
        orientation: {
          vertical: {
            container: 'mt-1'
          },
          horizontal: {
            root: 'flex justify-between place-items-baseline gap-2'
          }
        }
      },
      defaultVariants: {
        size: 'md',
        orientation: 'vertical'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/FormField.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/form-field.ts).


# Header

## Usage

The Header component renders a `<header>` element.

\> \[!TIP]
\> See: /docs/getting-started/theme/css-variables#header
\> Its height is defined through a \`--ui-header-height\` CSS variable.

Use the `left`, `default` and `right` slots to customize the header and the `body` or `content` slots to customize the header menu.

```vue [HeaderExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  active: route.path.startsWith('/docs/getting-started')
}, {
  label: 'Components',
  to: '/docs/components',
  active: route.path.startsWith('/docs/components')
}, {
  label: 'Figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}])
</script>

<template>
  <UHeader>
    <template #title>
      <Logo class="h-6 w-auto" />
    </template>

    <UNavigationMenu :items="items" />

    <template #right>
      <UColorModeButton />

      <UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
        <UButton
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/ui"
          target="_blank"
          icon="i-simple-icons-github"
          aria-label="GitHub"
        />
      </UTooltip>
    </template>
  </UHeader>
</template>
```

\> \[!NOTE]
\> In this example, we use the \[NavigationMenu]\(/docs/components/navigation-menu) component to render the header links in the center.

### Title

Use the `title` prop to change the title of the header. Defaults to `Nuxt UI`.

```vue
<template>
  <UHeader title="Nuxt UI" />
</template>
```

You can also use the `title` slot to add your own logo.

\> \[!TIP]
\> See: #props
\> You should still add the \`title\` prop to replace the default \`aria-label\` of the link.

```vue
<template>
  <UHeader>
    <template #title>
      <Logo class="h-6 w-auto" />
    </template></UHeader>
</template>
```

### To

Use the `to` prop to change the link of the title. Defaults to `/`.

```vue
<template>
  <UHeader to="/docs" />
</template>
```

You can also use the `left` slot to override the link entirely.

```vue
<template>
  <UHeader>
    <template #left>
      <NuxtLink to="/docs">
        <Logo class="h-6 w-auto" />
      </NuxtLink>
    </template></UHeader>
</template>
```

### Mode

Use the `mode` prop to change the mode of the header menu. Defaults to `modal`.

Use the `body` slot to fill the menu body (under the header) or the `content` slot to fill the entire menu.

\> \[!TIP]
\> See: #props
\> You can use the \`menu\` prop to customize the menu of the header, it will adapt depending on the mode you choose.

```vue [HeaderMenuExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  icon: 'i-lucide-book-open',
  active: route.path.startsWith('/docs/getting-started')
}, {
  label: 'Components',
  to: '/docs/components',
  icon: 'i-lucide-box',
  active: route.path.startsWith('/docs/components')
}, {
  label: 'Figma',
  icon: 'i-simple-icons-figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  icon: 'i-lucide-rocket',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}])
</script>

<template>
  <UHeader>
    <template #title>
      <Logo class="h-6 w-auto" />
    </template>

    <UNavigationMenu :items="items" />

    <template #right>
      <UColorModeButton />

      <UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
        <UButton
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/ui"
          target="_blank"
          icon="i-simple-icons-github"
          aria-label="GitHub"
        />
      </UTooltip>
    </template>

    <template #body>
      <UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
    </template>
  </UHeader>
</template>
```

### Toggle

Use the `toggle` prop to customize the toggle button displayed on mobile.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue [HeaderToggleExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  icon: 'i-lucide-book-open',
  active: route.path.startsWith('/docs/getting-started')
}, {
  label: 'Components',
  to: '/docs/components',
  icon: 'i-lucide-box',
  active: route.path.startsWith('/docs/components')
}, {
  label: 'Figma',
  icon: 'i-simple-icons-figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  icon: 'i-lucide-rocket',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}])
</script>

<template>
  <UHeader
    :toggle="{
      color: 'primary',
      variant: 'subtle',
      class: 'rounded-full'
    }"
  >
    <template #title>
      <Logo class="h-6 w-auto" />
    </template>

    <UNavigationMenu :items="items" />

    <template #right>
      <UColorModeButton />

      <UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
        <UButton
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/ui"
          target="_blank"
          icon="i-simple-icons-github"
          aria-label="GitHub"
        />
      </UTooltip>
    </template>

    <template #body>
      <UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
    </template>
  </UHeader>
</template>
```

### Toggle Side

Use the `toggle-side` prop to change the side of the toggle button. Defaults to `right`.

```vue [HeaderToggleSideExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  icon: 'i-lucide-book-open',
  active: route.path.startsWith('/docs/getting-started')
}, {
  label: 'Components',
  to: '/docs/components',
  icon: 'i-lucide-box',
  active: route.path.startsWith('/docs/components')
}, {
  label: 'Figma',
  icon: 'i-simple-icons-figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  icon: 'i-lucide-rocket',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}])
</script>

<template>
  <UHeader toggle-side="left">
    <template #title>
      <Logo class="h-6 w-auto" />
    </template>

    <UNavigationMenu :items="items" />

    <template #right>
      <UColorModeButton />

      <UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
        <UButton
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/ui"
          target="_blank"
          icon="i-simple-icons-github"
          aria-label="GitHub"
        />
      </UTooltip>
    </template>

    <template #body>
      <UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
    </template>
  </UHeader>
</template>
```

## Examples

### Within `app.vue`

Use the Header component in your `app.vue` or in a layout:

```vue [app.vue] {28-51}
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  active: route.path.startsWith('/docs/getting-started')
}, {
  label: 'Components',
  to: '/docs/components',
  active: route.path.startsWith('/docs/components')
}, {
  label: 'Figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}])
</script>

<template>
  <UApp>
    <UHeader>
      <template #title>
        <Logo class="h-6 w-auto" />
      </template>

      <UNavigationMenu :items="items" />

      <template #right>
        <UColorModeButton />

        <UButton
          color="neutral"
          variant="ghost"
          to="https://github.com/nuxt/ui"
          target="_blank"
          icon="i-simple-icons-github"
          aria-label="GitHub"
        />
      </template>

      <template #body>
        <UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
      </template>
    </UHeader>

    <UMain>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </UMain>

    <UFooter />
  </UApp>
</template>
```

## API

### Props

```ts
/**
 * Props for the Header component
 */
interface HeaderProps {
  /**
   * The element or component this component should render as.
   * @default "\"header\""
   */
  as?: any;
  /**
   * @default "\"Nuxt UI\""
   */
  title?: string | undefined;
  /**
   * @default "\"/\""
   */
  to?: string | undefined;
  /**
   * The mode of the header menu.
   * @default "\"modal\" as never"
   */
  mode?: T | undefined;
  /**
   * The props for the header menu component.
   */
  menu?: HeaderMenu<T> | undefined;
  /**
   * Customize the toggle button to open the header menu displayed when the `content` slot is used.
   * `{ color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  toggle?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The side to render the toggle button on.
   * @default "\"right\""
   */
  toggleSide?: "left" | "right" | undefined;
  /**
   * Automatically close when route changes.
   * @default "true"
   */
  autoClose?: boolean | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; left?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; title?: ClassNameValue; toggle?: ClassNameValue; content?: ClassNameValue; overlay?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; } | undefined;
  /**
   * @default "false"
   */
  open?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Header component
 */
interface HeaderSlots {
  title(): any;
  left(): any;
  default(): any;
  right(): any;
  toggle(): any;
  top(): any;
  bottom(): any;
  body(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Header component
 */
interface HeaderEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    header: {
      slots: {
        root: 'bg-default/75 backdrop-blur border-b border-default h-(--ui-header-height) sticky top-0 z-50',
        container: 'flex items-center justify-between gap-3 h-full',
        left: 'lg:flex-1 flex items-center gap-1.5',
        center: 'hidden lg:flex',
        right: 'flex items-center justify-end lg:flex-1 gap-1.5',
        title: 'shrink-0 font-bold text-xl text-highlighted flex items-end gap-1.5',
        toggle: 'lg:hidden',
        content: 'lg:hidden',
        overlay: 'lg:hidden',
        header: 'px-4 sm:px-6 h-(--ui-header-height) shrink-0 flex items-center justify-between gap-3',
        body: 'p-4 sm:p-6 overflow-y-auto'
      },
      variants: {
        toggleSide: {
          left: {
            toggle: '-ms-1.5'
          },
          right: {
            toggle: '-me-1.5'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Header.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/header.ts).


# Icon

## Usage

Use the `name` prop to display an icon:

```vue
<template>
  <UIcon name="i-lucide-lightbulb" class="size-5" />
</template>
```

\*\*Nuxt:\*\*
\> \[!CAUTION]
\> See: /docs/getting-started/integrations/icons/nuxt#collections
\> It's highly recommended to install the icons collections you need, read more about this.

## Examples

### SVG

You can also pass a Vue component into the `name` prop:

```vue [IconSvgExample.vue]
<script setup lang="ts">
import { h } from 'vue'

const IconLightbulb = () => h(
  'svg',
  { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24' },
  [
    h(
      'path',
      {
        'fill': 'none',
        'stroke': 'currentColor',
        'stroke-linecap': 'round',
        'stroke-linejoin': 'round',
        'stroke-width': 2,
        'd': 'M15 14c.2-1 .7-1.7 1.5-2.5c1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5c.7.7 1.3 1.5 1.5 2.5m0 4h6m-5 4h4'
      }
    )
  ]
)
</script>

<template>
  <UIcon :name="IconLightbulb" class="size-5" />
</template>
```

You can define your icon components yourself, or use [`unplugin-icons`](https://github.com/unplugin/unplugin-icons){rel="&#x22;nofollow&#x22;"} to import them directly from SVG files:

```vue
<script setup lang="ts">
import IconLightbulb from '~icons/lucide/lightbulb'
</script>

<template>
  <UIcon :name="IconLightbulb" class="size-5" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Icon component
 */
interface IconProps {
  name: any;
  mode?: "svg" | "css" | undefined;
  size?: string | number | undefined;
  customize?: boolean | IconifyIconCustomizeCallback | null | undefined;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Icon.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/icon.ts).


# Input

## Usage

Use the `v-model` directive to control the value of the Input.

```vue
<template>
  <UInput model-value="" />
</template>
```

### Type

Use the `type` prop to change the input type. Defaults to `text`.

Some types have been implemented in their own components such as [Checkbox](https://ui.nuxt.com/docs/components/checkbox), [Radio](https://ui.nuxt.com/docs/components/radio-group), [InputNumber](https://ui.nuxt.com/docs/components/input-number) etc. and others have been styled like `file` for example.

```vue
<template>
  <UInput type="file" />
</template>
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input\_types
\> You can check all the available types on the MDN Web Docs.

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<template>
  <UInput placeholder="Search..." />
</template>
```

### Color

Use the `color` prop to change the ring color when the Input is focused.

```vue
<template>
  <UInput color="neutral" highlight placeholder="Search..." />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the Input.

```vue
<template>
  <UInput color="neutral" variant="subtle" :highlight="false" placeholder="Search..." />
</template>
```

### Size

Use the `size` prop to change the size of the Input.

```vue
<template>
  <UInput size="xl" placeholder="Search..." />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the Input.

```vue
<template>
  <UInput icon="i-lucide-search" size="md" variant="outline" placeholder="Search..." />
</template>
```

Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.

```vue
<template>
  <UInput trailing-icon="i-lucide-at-sign" placeholder="Enter your email" size="md" />
</template>
```

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the Input.

```vue
<template>
  <UInput size="md" variant="outline" placeholder="Search..." />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the Input.

```vue
<template>
  <UInput loading :trailing="false" placeholder="Search..." />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <UInput loading loading-icon="i-lucide-loader" placeholder="Search..." />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the Input.

```vue
<template>
  <UInput disabled placeholder="Search..." />
</template>
```

## Examples

### With clear button

You can put a [Button](https://ui.nuxt.com/docs/components/button) inside the `#trailing` slot to clear the Input.

```vue [InputClearButtonExample.vue]
<script setup lang="ts">
const value = ref('Click to clear')
</script>

<template>
  <UInput
    v-model="value"
    placeholder="Type something..."
    :ui="{ trailing: 'pe-1' }"
  >
    <template v-if="value?.length" #trailing>
      <UButton
        color="neutral"
        variant="link"
        size="sm"
        icon="i-lucide-circle-x"
        aria-label="Clear input"
        @click="value = ''"
      />
    </template>
  </UInput>
</template>
```

### With copy button

You can put a [Button](https://ui.nuxt.com/docs/components/button) inside the `#trailing` slot to copy the value to the clipboard.

```vue [InputCopyButtonExample.vue]
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'

const value = ref('npx nuxt module add ui')

const { copy, copied } = useClipboard()
</script>

<template>
  <UInput
    v-model="value"
    :ui="{ trailing: 'pr-0.5' }"
  >
    <template v-if="value?.length" #trailing>
      <UTooltip text="Copy to clipboard" :content="{ side: 'right' }">
        <UButton
          :color="copied ? 'success' : 'neutral'"
          variant="link"
          size="sm"
          :icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
          aria-label="Copy to clipboard"
          @click="copy(value)"
        />
      </UTooltip>
    </template>
  </UInput>
</template>
```

### With password toggle

You can put a [Button](https://ui.nuxt.com/docs/components/button) inside the `#trailing` slot to toggle the password visibility.

```vue [InputPasswordToggleExample.vue]
<script setup lang="ts">
const show = ref(false)
const password = ref('')
</script>

<template>
  <UInput
    v-model="password"
    placeholder="Password"
    :type="show ? 'text' : 'password'"
    :ui="{ trailing: 'pe-1' }"
  >
    <template #trailing>
      <UButton
        color="neutral"
        variant="link"
        size="sm"
        :icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
        :aria-label="show ? 'Hide password' : 'Show password'"
        :aria-pressed="show"
        aria-controls="password"
        @click="show = !show"
      />
    </template>
  </UInput>
</template>

<style>
/* Hide the password reveal button in Edge */
::-ms-reveal {
    display: none;
}
</style>
```

### With password strength indicator

You can use the [Progress](https://ui.nuxt.com/docs/components/progress) component to display the password strength indicator.

```vue [InputPasswordStrengthIndicatorExample.vue]
<script setup lang="ts">
const show = ref(false)
const password = ref('')

function checkStrength(str: string) {
  const requirements = [
    { regex: /.{8,}/, text: 'At least 8 characters' },
    { regex: /\d/, text: 'At least 1 number' },
    { regex: /[a-z]/, text: 'At least 1 lowercase letter' },
    { regex: /[A-Z]/, text: 'At least 1 uppercase letter' }
  ]

  return requirements.map(req => ({ met: req.regex.test(str), text: req.text }))
}

const strength = computed(() => checkStrength(password.value))
const score = computed(() => strength.value.filter(req => req.met).length)

const color = computed(() => {
  if (score.value === 0) return 'neutral'
  if (score.value <= 1) return 'error'
  if (score.value <= 2) return 'warning'
  if (score.value === 3) return 'warning'
  return 'success'
})

const text = computed(() => {
  if (score.value === 0) return 'Enter a password'
  if (score.value <= 2) return 'Weak password'
  if (score.value === 3) return 'Medium password'
  return 'Strong password'
})
</script>

<template>
  <div class="space-y-2">
    <UFormField label="Password">
      <UInput
        v-model="password"
        placeholder="Password"
        :color="color"
        :type="show ? 'text' : 'password'"
        :aria-invalid="score < 4"
        aria-describedby="password-strength"
        :ui="{ trailing: 'pe-1' }"
        class="w-full"
      >
        <template #trailing>
          <UButton
            color="neutral"
            variant="link"
            size="sm"
            :icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
            :aria-label="show ? 'Hide password' : 'Show password'"
            :aria-pressed="show"
            aria-controls="password"
            @click="show = !show"
          />
        </template>
      </UInput>
    </UFormField>

    <UProgress
      :color="color"
      :indicator="text"
      :model-value="score"
      :max="4"
      size="sm"
    />

    <p id="password-strength" class="text-sm font-medium">
      {{ text }}. Must contain:
    </p>

    <ul class="space-y-1" aria-label="Password requirements">
      <li
        v-for="(req, index) in strength"
        :key="index"
        class="flex items-center gap-0.5"
        :class="req.met ? 'text-success' : 'text-muted'"
      >
        <UIcon :name="req.met ? 'i-lucide-circle-check' : 'i-lucide-circle-x'" class="size-4 shrink-0" />

        <span class="text-xs font-light">
          {{ req.text }}
          <span class="sr-only">
            {{ req.met ? ' - Requirement met' : ' - Requirement not met' }}
          </span>
        </span>
      </li>
    </ul>
  </div>
</template>
```

### With character limit

You can use the `#trailing` slot to add a character limit to the Input.

```vue [InputCharacterLimitExample.vue]
<script setup lang="ts">
const value = ref('')
const maxLength = 15
</script>

<template>
  <UInput
    v-model="value"
    :maxlength="maxLength"
    aria-describedby="character-count"
    :ui="{ trailing: 'pointer-events-none' }"
  >
    <template #trailing>
      <div
        id="character-count"
        class="text-xs text-muted tabular-nums"
        aria-live="polite"
        role="status"
      >
        {{ value?.length }}/{{ maxLength }}
      </div>
    </template>
  </UInput>
</template>
```

### With keyboard shortcut

You can use the [Kbd](https://ui.nuxt.com/docs/components/kbd) component inside the `#trailing` slot to add a keyboard shortcut to the Input.

```vue [InputKbdExample.vue]
<script setup lang="ts">
const input = useTemplateRef('input')

defineShortcuts({
  '/': () => {
    input.value?.inputRef?.focus()
  }
})
</script>

<template>
  <UInput
    ref="input"
    icon="i-lucide-search"
    placeholder="Search..."
  >
    <template #trailing>
      <UKbd value="/" />
    </template>
  </UInput>
</template>
```

\> \[!NOTE]
\> See: /composables/define-shortcuts
\> This example uses the \`defineShortcuts\` composable to focus the Input when the key is pressed.

### With mask

There's no built-in support for masks, but you can use libraries like [maska](https://github.com/beholdr/maska){rel="&#x22;nofollow&#x22;"} to mask the Input.

```vue [InputMaskExample.vue]
<script setup lang="ts">
import { vMaska } from 'maska/vue'
</script>

<template>
  <div class="flex flex-col gap-2">
    <UInput v-maska="'#### #### #### ####'" placeholder="4242 4242 4242 4242" icon="i-lucide-credit-card" />

    <div class="flex items-center gap-2">
      <UInput v-maska="'##/##'" placeholder="MM/YY" icon="i-lucide-calendar" />
      <UInput v-maska="'###'" placeholder="CVC" />
    </div>
  </div>
</template>
```

### With floating label

You can use the `#default` slot to add a floating label to the Input.

```vue [InputFloatingLabelExample.vue]
<script setup lang="ts">
const value = ref('')
</script>

<template>
  <UInput v-model="value" placeholder="" :ui="{ base: 'peer' }">
    <label class="pointer-events-none absolute left-0 -top-2.5 text-highlighted text-xs font-medium px-1.5 transition-all peer-focus:-top-2.5 peer-focus:text-highlighted peer-focus:text-xs peer-focus:font-medium peer-placeholder-shown:text-sm peer-placeholder-shown:text-dimmed peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal">
      <span class="inline-flex bg-default px-1">Email address</span>
    </label>
  </UInput>
</template>
```

### Within a FormField

You can use the Input within a [FormField](https://ui.nuxt.com/docs/components/form-field) component to display a label, help text, required indicator, etc.

```vue [InputFormFieldExample.vue]
<script setup lang="ts">
const email = ref('')
</script>

<template>
  <UFormField label="Email" help="We won't share your email." required>
    <UInput v-model="email" placeholder="Enter your email" icon="i-lucide-at-sign" />
  </UFormField>
</template>
```

\> \[!TIP]
\> See: /docs/components/form
\> It also provides validation and error handling when used within a Form component.

### Within a FieldGroup

You can use the Input within a [FieldGroup](https://ui.nuxt.com/components/field-group) component to group multiple elements together.

```vue [InputFieldGroupExample.vue]
<script setup lang="ts">
const value = ref('')
const domains = ['.com', '.dev', '.org']
const domain = ref(domains[0])
</script>

<template>
  <UFieldGroup>
    <UInput
      v-model="value"
      placeholder="nuxt"
      :ui="{
        base: 'pl-14.5',
        leading: 'pointer-events-none'
      }"
    >
      <template #leading>
        <p class="text-sm text-muted">
          https://
        </p>
      </template>
    </UInput>

    <USelectMenu v-model="domain" :items="domains" />
  </UFieldGroup>
</template>
```

### As a phone number input

You can use the Input within a [FieldGroup](https://ui.nuxt.com/docs/components/field-group) component alongside a [SelectMenu](https://ui.nuxt.com/docs/components/select-menu) to create a phone number input with country code selection.

```vue [InputPhoneNumberExample.vue]
<script setup lang="ts">
import { vMaska } from 'maska/vue'

type PhoneCode = {
  name: string
  code: string
  emoji: string
  dialCode: string
  mask: string
}

const phone = ref('')
const countryCode = ref('US')

const { data: phoneCodes, status, execute } = await useLazyFetch<PhoneCode[]>('/api/phone-codes.json', {
  key: 'api-phone-codes',
  immediate: false
})

const country = computed(() => phoneCodes.value?.find(c => c.code === countryCode.value))
const dialCode = computed(() => country.value?.dialCode || '+1')
const mask = computed(() => country.value?.mask || '(###) ###-####')

function onOpen() {
  if (!phoneCodes.value?.length) {
    execute()
  }
}

watch(countryCode, () => {
  phone.value = ''
})
</script>

<template>
  <UFieldGroup>
    <USelectMenu
      v-model="countryCode"
      :items="phoneCodes"
      value-key="code"
      :search-input="{
        placeholder: 'Search country...',
        icon: 'i-lucide-search',
        loading: status === 'pending'
      }"
      :filter-fields="['name', 'code', 'dialCode']"
      :content="{ align: 'start' }"
      :ui="{
        base: 'pe-8',
        content: 'w-48',
        placeholder: 'hidden',
        trailingIcon: 'size-4'
      }"
      trailing-icon="i-lucide-chevrons-up-down"
      @update:open="onOpen"
    >
      <span class="size-5 flex items-center text-lg">
        {{ country?.emoji || '\u{1F1FA}\u{1F1F8}' }}
      </span>

      <template #item-leading="{ item }">
        <span class="size-5 flex items-center text-lg">
          {{ item.emoji }}
        </span>
      </template>

      <template #item-label="{ item }">
        {{ item.name }} ({{ item.dialCode }})
      </template>
    </USelectMenu>

    <UInput
      v-model="phone"
      v-maska="mask"
      :placeholder="mask.replaceAll('#', '_')"
      :style="{ '--dial-code-length': `${dialCode.length + 1.5}ch` }"
      :ui="{
        base: 'ps-(--dial-code-length)',
        leading: 'pointer-events-none text-base md:text-sm text-muted'
      }"
    >
      <template #leading>
        {{ dialCode }}
      </template>
    </UInput>
  </UFieldGroup>
</template>
```

## API

### Props

```ts
/**
 * Props for the Input component
 */
interface InputProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  id?: string | undefined;
  name?: string | undefined;
  /**
   * @default "\"text\""
   */
  type?: InputTypeHTMLAttribute | undefined;
  /**
   * The placeholder text when the input is empty.
   */
  placeholder?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  required?: boolean | undefined;
  /**
   * @default "\"off\""
   */
  autocomplete?: (string & {}) | "on" | "off" | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  disabled?: boolean | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  modelValue?: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod> | undefined;
  defaultValue?: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod> | undefined;
  modelModifiers?: Mod | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; } | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  enterKeyHint?: "search" | "enter" | "done" | "go" | "next" | "previous" | "send" | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  list?: string | undefined;
  max?: Numberish | undefined;
  maxlength?: Numberish | undefined;
  min?: Numberish | undefined;
  minlength?: Numberish | undefined;
  pattern?: string | undefined;
  readonly?: Booleanish | undefined;
  step?: Numberish | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
\> This component also supports all native \`\<input>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Input component
 */
interface InputSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Input component
 */
interface InputEmits {
  update:modelValue: (payload: [value: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod>]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  change: (payload: [event: Event]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                           | Type                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLInputElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    input: {
      slots: {
        root: 'relative inline-flex items-center',
        base: [
          'w-full rounded-md border-0 appearance-none placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed'
      },
      variants: {
        fieldGroup: {
          horizontal: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none'
          },
          vertical: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none'
          }
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-sm/4 gap-1',
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-sm/4 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          md: {
            base: 'px-2.5 py-1.5 text-base/5 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          lg: {
            base: 'px-3 py-2 text-base/5 gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Input.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input.ts).


# InputDate

## Usage

Use the `v-model` directive to control the selected date.

```vue
<template>
  <UInputDate />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UInputDate />
</template>
```

\*\*Nuxt:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The date format is determined by the \`locale\` prop of the App component.
\*\*Vue:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/vue#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The date format is determined by the \`locale\` prop of the App component.

### Range

Use the `range` prop to select a range of dates.

```vue
<template>
  <UInputDate range />
</template>
```

### Color

Use the `color` prop to change the color of the InputDate.

```vue
<template>
  <UInputDate color="neutral" highlight />
</template>
```

### Variant

Use the `variant` prop to change the variant of the InputDate.

```vue
<template>
  <UInputDate variant="subtle" />
</template>
```

### Size

Use the `size` prop to change the size of the InputDate.

```vue
<template>
  <UInputDate size="xl" />
</template>
```

### Separator Icon

Use the `separator-icon` prop to change the icon of the range separator.

```vue
<template>
  <UInputDate range separator-icon="i-lucide-arrow-right" />
</template>
```

### Disabled

Use the `disabled` prop to disable the InputDate.

```vue
<template>
  <UInputDate disabled />
</template>
```

## Examples

### With unavailable dates

Use the `is-date-unavailable` prop with a function to mark specific dates as unavailable.

```vue [InputDateUnavailableDatesExample.vue]
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateUnavailable = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UInputDate v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
```

### With min/max dates

Use the `min-value` and `max-value` props to limit the dates.

```vue [InputDateMinMaxDatesExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>

<template>
  <UInputDate v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>
```

### As a date picker

Use a [Calendar](https://ui.nuxt.com/docs/components/calendar) and a [Popover](https://ui.nuxt.com/docs/components/popover) component to create a date picker.

```vue [InputDateDatePickerExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const inputDate = useTemplateRef('inputDate')

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>

<template>
  <UInputDate ref="inputDate" v-model="modelValue">
    <template #trailing>
      <UPopover :reference="inputDate?.inputsRef[3]?.$el">
        <UButton
          color="neutral"
          variant="link"
          size="sm"
          icon="i-lucide-calendar"
          aria-label="Select a date"
          class="px-0"
        />

        <template #content>
          <UCalendar v-model="modelValue" class="p-2" />
        </template>
      </UPopover>
    </template>
  </UInputDate>
</template>
```

### As a date range picker

Use a [Calendar](https://ui.nuxt.com/docs/components/calendar) and a [Popover](https://ui.nuxt.com/docs/components/popover) component to create a date range picker.

```vue [InputDateDateRangePickerExample.vue]
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const inputDate = useTemplateRef('inputDate')

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 10),
  end: new CalendarDate(2022, 1, 20)
})
</script>

<template>
  <UInputDate ref="inputDate" v-model="modelValue" range>
    <template #trailing>
      <UPopover :reference="inputDate?.inputsRef[0]?.$el">
        <UButton
          color="neutral"
          variant="link"
          size="sm"
          icon="i-lucide-calendar"
          aria-label="Select a date range"
          class="px-0"
        />

        <template #content>
          <UCalendar v-model="modelValue" class="p-2" :number-of-months="2" range />
        </template>
      </UPopover>
    </template>
  </UInputDate>
</template>
```

## API

### Props

```ts
/**
 * Props for the InputDate component
 */
interface InputDateProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  /**
   * The icon to use as a range separator.
   */
  separatorIcon?: any;
  /**
   * Whether or not a range of dates can be selected
   */
  range?: R | undefined;
  defaultValue?: CalendarDate | CalendarDateTime | ZonedDateTime | DateRange;
  modelValue?: null | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; segment?: ClassNameValue; separatorIcon?: ClassNameValue; } | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  defaultPlaceholder?: CalendarDate | CalendarDateTime | ZonedDateTime;
  placeholder?: CalendarDate | CalendarDateTime | ZonedDateTime;
  /**
   * The hour cycle used for formatting times. Defaults to the local preference
   */
  hourCycle?: HourCycle;
  /**
   * The stepping interval for the time fields. Defaults to `1`.
   */
  step?: DateStep | undefined;
  /**
   * The granularity to use for formatting times. Defaults to day if a CalendarDate is provided, otherwise defaults to minute. The field will render segments for each part of the date up to and including the specified granularity
   */
  granularity?: Granularity | undefined;
  /**
   * Whether or not to hide the time zone segment of the field
   */
  hideTimeZone?: boolean | undefined;
  maxValue?: CalendarDate | CalendarDateTime | ZonedDateTime;
  minValue?: CalendarDate | CalendarDateTime | ZonedDateTime;
  /**
   * Whether or not the date field is disabled
   */
  disabled?: boolean | undefined;
  /**
   * Whether or not the date field is readonly
   */
  readonly?: boolean | undefined;
  /**
   * A function that returns whether or not a date is unavailable
   */
  isDateUnavailable?: Matcher | undefined;
  /**
   * Id of the element
   */
  id?: string | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the InputDate component
 */
interface InputDateSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  separator(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the InputDate component
 */
interface InputDateEmits {
  update:modelValue: (payload: [date: InputDateModelValue<R>]) => void;
  update:placeholder: (payload: [date: DateValue] & [date: DateValue]) => void;
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    inputDate: {
      slots: {
        base: [
          'group relative inline-flex items-center rounded-md select-none',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed',
        segment: [
          'rounded text-center outline-hidden data-placeholder:text-dimmed data-[segment=literal]:text-muted data-invalid:text-error data-disabled:cursor-not-allowed data-disabled:opacity-75',
          'transition-colors'
        ],
        separatorIcon: 'shrink-0 size-4 text-muted'
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        size: {
          xs: {
            base: [
              'px-2 py-1 text-sm/4 gap-1',
              'gap-0.25'
            ],
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            segment: 'data-[segment=day]:w-6 data-[segment=month]:w-6 data-[segment=year]:w-9'
          },
          sm: {
            base: [
              'px-2.5 py-1.5 text-sm/4 gap-1.5',
              'gap-0.5'
            ],
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            segment: 'data-[segment=day]:w-6 data-[segment=month]:w-6 data-[segment=year]:w-9'
          },
          md: {
            base: [
              'px-2.5 py-1.5 text-base/5 gap-1.5',
              'gap-0.5'
            ],
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            segment: 'data-[segment=day]:w-7 data-[segment=month]:w-7 data-[segment=year]:w-11'
          },
          lg: {
            base: [
              'px-3 py-2 text-base/5 gap-2',
              'gap-0.75'
            ],
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            segment: 'data-[segment=day]:w-7 data-[segment=month]:w-7 data-[segment=year]:w-11'
          },
          xl: {
            base: [
              'px-3 py-2 text-base gap-2',
              'gap-0.75'
            ],
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            segment: 'data-[segment=day]:w-8 data-[segment=month]:w-8 data-[segment=year]:w-13'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        }
      },
      compoundVariants: [
        {
          variant: 'outline',
          class: {
            segment: 'focus:bg-elevated'
          }
        },
        {
          variant: 'soft',
          class: {
            segment: 'focus:bg-accented/50 group-hover:focus:bg-accented'
          }
        },
        {
          variant: 'subtle',
          class: {
            segment: 'focus:bg-accented'
          }
        },
        {
          variant: 'ghost',
          class: {
            segment: 'focus:bg-elevated group-hover:focus:bg-accented'
          }
        },
        {
          variant: 'none',
          class: {
            segment: 'focus:bg-elevated'
          }
        },
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/InputDate.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input-date.ts).


# InputMenu

## Usage

Use the `v-model` directive to control the value of the InputMenu or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" :items="items" />
</template>
```

\> \[!TIP]
\> Use this over an \[\`Input\`]\(/docs/components/input) to take advantage of Reka UI's \[\`Combobox\`]\(https\://reka-ui.com/docs/components/combobox) component that offers autocomplete capabilities.

\> \[!NOTE]
\> This component is similar to the \[\`SelectMenu\`]\(/docs/components/select-menu) but it's using an Input instead of a Select.

### Items

Use the `items` prop as an array of strings, numbers or booleans:

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" :items="items" />
</template>
```

You can also pass an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`type?: "label" | "separator" | "item"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-items-type)
- [`icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-icons-in-items)
- [`avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-avatar-in-items)
- [`chip?: ChipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-chip-in-items)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { tagsItem?: ClassNameValue, tagsItemText?: ClassNameValue, tagsItemDelete?: ClassNameValue, tagsItemDeleteIcon?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items = ref<InputMenuItem[]>([
  {
    label: 'Backlog',
  },
  {
    label: 'Todo',
  },
  {
    label: 'In Progress',
  },
  {
    label: 'Done',
  },
])
</script>

<template>
  <UInputMenu :items="items" />
</template>
```

You can also pass an array of arrays to the `items` prop to display separated groups of items.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  [
    'Apple',
    'Banana',
    'Blueberry',
    'Grapes',
    'Pineapple',
  ],
  [
    'Aubergine',
    'Broccoli',
    'Carrot',
    'Courgette',
    'Leek',
  ],
])
</script>

<template>
  <UInputMenu model-value="Apple" :items="items" />
</template>
```

### Value Key

You can choose to bind a single property of the object rather than the whole object by using the `value-key` prop. Defaults to `undefined`.

```vue
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items = ref<InputMenuItem[]>([
  {
    label: 'Backlog',
    id: 'backlog',
  },
  {
    label: 'Todo',
    id: 'todo',
  },
  {
    label: 'In Progress',
    id: 'in_progress',
  },
  {
    label: 'Done',
    id: 'done',
  },
])
</script>

<template>
  <UInputMenu model-value="todo" value-key="id" :items="items" />
</template>
```

\> \[!TIP]
\> Use the \`by\` prop to compare objects by a field instead of reference when the \`model-value\` is an object.

### Multiple

Use the `multiple` prop to allow multiple selections, the selected items will be displayed as tags.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu multiple :items="items" />
</template>
```

\> \[!CAUTION]
\> Ensure to pass an array to the \`default-value\` prop or the \`v-model\` directive.

### Delete Icon

With `multiple`, use the `delete-icon` prop to customize the delete [Icon](https://ui.nuxt.com/docs/components/icon) in the tags. Defaults to `i-lucide-x`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu multiple delete-icon="i-lucide-trash" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu placeholder="Select status" :items="items" />
</template>
```

### Autocomplete `Soon`

Use the `autocomplete` prop to turn the InputMenu into a free-form text input with suggestions. The `modelValue` becomes the input text (`string`) instead of a selected item.

```vue [InputMenuAutocompleteExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('')
</script>

<template>
  <UInputMenu
    v-model="value"
    autocomplete
    :items="items"
    :trailing-icon="false"
    :content="{ hideWhenEmpty: true }"
    placeholder="Type a status..."
  />
</template>
```

\> \[!CAUTION]
\> When \`autocomplete\` is \`true\`, \`multiple\`, \`by\`, \`resetSearchTermOnSelect\` and \`resetModelValueOnClear\` are not applicable.

\> \[!TIP]
\> Use the \`content.hideWhenEmpty\` prop to hide the menu when there are no matching suggestions.

### Content

Use the `content` prop to control how the InputMenu content is rendered, like its `align` or `side` for example.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" :items="items" />
</template>
```

### Arrow

Use the `arrow` prop to display an arrow on the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" arrow :items="items" />
</template>
```

### Color

Use the `color` prop to change the ring color when the InputMenu is focused.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" color="neutral" highlight :items="items" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" color="neutral" variant="subtle" :highlight="false" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" size="xl" :items="items" />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" icon="i-lucide-search" size="md" :items="items" />
</template>
```

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-chevron-down`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" trailing-icon="i-lucide-arrow-down" size="md" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Selected Icon

Use the `selected-icon` prop to customize the icon when an item is selected. Defaults to `i-lucide-check`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" selected-icon="i-lucide-flame" size="md" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.check\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.check\` key.

### Clear `4.4+`

Use the `clear` prop to display a clear button when a value is selected.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" clear :items="items" />
</template>
```

### Clear Icon `4.4+`

Use the `clear-icon` prop to customize the clear button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" clear clear-icon="i-lucide-trash" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Nuxt',
  'NuxtHub',
  'NuxtLabs',
  'Nuxt Modules',
  'Nuxt Community',
])
</script>

<template>
  <UInputMenu model-value="Nuxt" :items="items" />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" loading :trailing="false" :items="items" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu model-value="Backlog" loading loading-icon="i-lucide-loader" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the InputMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <UInputMenu disabled placeholder="Select status" :items="items" />
</template>
```

## Examples

### With items type

You can use the `type` property with `separator` to display a separator between items or `label` to display a label.

```vue
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items = ref<InputMenuItem[]>([
  {
    type: 'label',
    label: 'Fruits',
  },
  'Apple',
  'Banana',
  'Blueberry',
  'Grapes',
  'Pineapple',
  {
    type: 'separator',
  },
  {
    type: 'label',
    label: 'Vegetables',
  },
  'Aubergine',
  'Broccoli',
  'Carrot',
  'Courgette',
  'Leek',
])
</script>

<template>
  <UInputMenu model-value="Apple" :items="items" />
</template>
```

### With icon in items

You can use the `icon` property to display an [Icon](https://ui.nuxt.com/docs/components/icon) inside the items.

```vue [InputMenuItemsIconExample.vue]
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'Backlog',
    value: 'backlog',
    icon: 'i-lucide-circle-help'
  },
  {
    label: 'Todo',
    value: 'todo',
    icon: 'i-lucide-circle-plus'
  },
  {
    label: 'In Progress',
    value: 'in_progress',
    icon: 'i-lucide-circle-arrow-up'
  },
  {
    label: 'Done',
    value: 'done',
    icon: 'i-lucide-circle-check'
  }
] satisfies InputMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <UInputMenu v-model="value" :icon="value?.icon" :items="items" />
</template>
```

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected icon.

### With avatar in items

You can use the `avatar` property to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the items.

```vue [InputMenuItemsAvatarExample.vue]
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'benjamincanac',
    value: 'benjamincanac',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  },
  {
    label: 'romhml',
    value: 'romhml',
    avatar: {
      src: 'https://github.com/romhml.png',
      alt: 'romhml',
      loading: 'lazy' as const
    }
  },
  {
    label: 'noook',
    value: 'noook',
    avatar: {
      src: 'https://github.com/noook.png',
      alt: 'noook',
      loading: 'lazy' as const
    }
  },
  {
    label: 'sandros94',
    value: 'sandros94',
    avatar: {
      src: 'https://github.com/sandros94.png',
      alt: 'sandros94',
      loading: 'lazy' as const
    }
  }
] satisfies InputMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <UInputMenu v-model="value" :avatar="value?.avatar" :items="items" />
</template>
```

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected avatar.

### With chip in items

You can use the `chip` property to display a [Chip](https://ui.nuxt.com/docs/components/chip) inside the items.

```vue [InputMenuItemsChipExample.vue]
<script setup lang="ts">
import type { InputMenuItem, ChipProps } from '@nuxt/ui'

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error'
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success'
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info'
    }
  }
] satisfies InputMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <UInputMenu v-model="value" :items="items">
    <template #leading="{ modelValue, ui }">
      <UChip
        v-if="modelValue"
        v-bind="modelValue.chip"
        inset
        standalone
        :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
        :class="ui.itemLeadingChip()"
      />
    </template>
  </UInputMenu>
</template>
```

\> \[!NOTE]
\> In this example, the \`#leading\` slot is used to display the selected chip.

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [InputMenuOpenExample.vue]
<script setup lang="ts">
const open = ref(false)
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UInputMenu v-model="value" v-model:open="open" :items="items" />
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the InputMenu by pressing .

### Control open state on focus

You can use the `open-on-focus` or `open-on-click` props to open the menu when the input is focused or clicked.

```vue [InputMenuOpenFocusExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const selected = ref('Backlog')
</script>

<template>
  <UInputMenu
    v-model="selected"
    :items="items"
    open-on-focus
  />
</template>
```

### Control search term

Use the `v-model:search-term` directive to control the search term.

```vue [InputMenuSearchTermExample.vue]
<script setup lang="ts">
const searchTerm = ref('D')
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')
</script>

<template>
  <UInputMenu v-model="value" v-model:search-term="searchTerm" :items="items" />
</template>
```

### With rotating icon

Here is an example with a rotating icon that indicates the open state of the InputMenu.

```vue [InputMenuIconExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')
</script>

<template>
  <UInputMenu
    v-model="value"
    :items="items"
    :ui="{
      trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
    }"
  />
</template>
```

### With create item

Use the `create-item` prop to enable users to add custom values that aren't in the predefined options.

```vue [InputMenuCreateItemExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')

function onCreate(item: string) {
  items.value.push(item)

  value.value = item
}
</script>

<template>
  <UInputMenu
    v-model="value"
    create-item
    :items="items"
    class="w-48"
    @create="onCreate"
  />
</template>
```

\> \[!NOTE]
\> The create option shows when no match is found by default. Set it to \`always\` to show it even when similar values exist.

\> \[!TIP]
\> See: #emits
\> Use the \`@create\` event to handle the creation of the item. You will receive the event and the item as arguments.

### With fetched items

You can fetch items from an API and use them in the InputMenu.

```vue [InputMenuFetchExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users',
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UInputMenu
    :items="users"
    :loading="status === 'pending'"
    icon="i-lucide-user"
    placeholder="Select user"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>
  </UInputMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the menu opens, avoiding unnecessary API calls on page load.

### With ignore filter

Set the `ignore-filter` prop to `true` to disable the internal search and use your own search logic.

```vue [InputMenuIgnoreFilterExample.vue]
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'
import type { AvatarProps } from '@nuxt/ui'

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'input-menu-users-search',
  params: { q: searchTermDebounced },
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UInputMenu
    v-model:search-term="searchTerm"
    :items="users"
    :loading="status === 'pending'"
    ignore-filter
    icon="i-lucide-user"
    placeholder="Select user"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>
  </UInputMenu>
</template>
```

\> \[!NOTE]
\> This example uses \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/#refdebounced) to debounce the API calls. The fetch is deferred with \`immediate: false\` so no request is made until the menu opens.

### With filter fields

Use the `filter-fields` prop with an array of fields to filter on. Defaults to `[labelKey]`.

```vue [InputMenuFilterFieldsExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users-email',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      email: user.email,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UInputMenu
    :items="users"
    :loading="status === 'pending'"
    :filter-fields="['label', 'email']"
    icon="i-lucide-user"
    placeholder="Select user"
    class="w-80"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>

    <template #item-label="{ item }">
      {{ item.label }}

      <span class="text-muted">
        {{ item.email }}
      </span>
    </template>
  </UInputMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the menu opens, avoiding unnecessary API calls on page load.

### With virtualization `4.1+`

Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.

\> \[!WARNING]
\> See: https\://github.com/unovue/reka-ui/issues/1885
\> When enabled, all groups are flattened into a single list due to a limitation of Reka UI.

```vue [InputMenuVirtualizeExample.vue]
<script setup lang="ts">
import type { InputMenuItem } from '@nuxt/ui'

const items: InputMenuItem[] = Array(1000).fill(0).map((_, i) => ({
  label: `item-${i}`,
  value: i
}))
</script>

<template>
  <UInputMenu virtualize :items="items" class="w-48" />
</template>
```

### With infinite scroll `4.4+`

You can use the [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/){rel="&#x22;nofollow&#x22;"} composable to load more data as the user scrolls.

```vue [InputMenuInfiniteScrollExample.vue]
<script setup lang="ts">
import { useInfiniteScroll } from '@vueuse/core'

type User = {
  firstName: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status, execute } = await useLazyFetch('https://dummyjson.com/users?limit=10&select=firstName', {
  key: 'input-menu-users-infinite-scroll',
  params: { skip },
  transform: (data?: UserResponse) => {
    return data?.users.map(user => user.firstName)
  },
  immediate: false
})

const users = ref<string[]>([])

watch(data, () => {
  users.value = [
    ...users.value,
    ...(data.value || [])
  ]
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}

const inputMenu = useTemplateRef('inputMenu')

onMounted(() => {
  useInfiniteScroll(() => inputMenu.value?.viewportRef, () => {
    skip.value += 10
  }, {
    canLoadMore: () => {
      return status.value !== 'pending'
    }
  })
})
</script>

<template>
  <UInputMenu
    ref="inputMenu"
    placeholder="Select user"
    :items="users"
    @update:open="onOpen"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` so data is only loaded as the user scrolls.

### With full content width

You can expand the content to the full width of its items by adding the `min-w-fit` class on the `ui.content` slot.

```vue [InputMenuContentWidthExample.vue]
<script setup lang="ts">
const { data: users, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users-email',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      email: user.email,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UInputMenu
    :items="users"
    icon="i-lucide-user"
    placeholder="Select user"
    :ui="{ content: 'min-w-fit' }"
    @update:open="onOpen"
  >
    <template #item-label="{ item }">
      {{ item.label }}

      <span class="text-muted">
        {{ item.email }}
      </span>
    </template>
  </UInputMenu>
</template>
```

\> \[!TIP]
\> You can also change the content width globally in your \`app.config.ts\`:
\> \`\`\`text
\> export default defineAppConfig({
\> ui: {
\> inputMenu: {
\> slots: {
\> content: 'min-w-fit'
\> }
\> }
\> }
\> })
\>
\> \`\`\`

### As a country picker

You can use the InputMenu as a country picker with lazy loading. Countries are only fetched when the menu is first opened.

```vue [InputMenuCountriesExample.vue]
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
  name: string
  code: string
  emoji: string
}[]>('/api/countries.json', {
  key: 'api-countries',
  immediate: false
})

function onOpen() {
  if (!countries.value?.length) {
    execute()
  }
}
</script>

<template>
  <UInputMenu
    :items="countries"
    :loading="status === 'pending'"
    label-key="name"
    :search-input="{ icon: 'i-lucide-search' }"
    placeholder="Select country"
    class="w-48"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <span v-if="modelValue" class="size-5 text-center">
        {{ modelValue?.emoji }}
      </span>
      <UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
    </template>
    <template #item-leading="{ item }">
      <span class="size-5 text-center">
        {{ item.emoji }}
      </span>
    </template>
  </UInputMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only load countries when the menu is first opened.

## API

### Props

```ts
/**
 * Props for the InputMenu component
 */
interface InputMenuProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  id?: string | undefined;
  /**
   * @default "\"text\""
   */
  type?: InputTypeHTMLAttribute | undefined;
  /**
   * The placeholder text when the input is empty.
   */
  placeholder?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "soft" | "outline" | "subtle" | "ghost" | "none" | undefined;
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  required?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * The icon displayed to delete a tag.
   * Works only when `multiple` is `true`.
   */
  deleteIcon?: any;
  /**
   * Display a clear button to reset the model value.
   * Can be an object to pass additional props to the Button.
   */
  clear?: (C & boolean) | (C & Partial<Omit<ButtonProps, LinkPropsKeys>>) | undefined;
  /**
   * The icon displayed in the clear button.
   */
  clearIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<ComboboxContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DismissableLayerEmits>>) | undefined;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<ComboboxArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the menu in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, all groups are flattened into a single list due to a limitation of Reka UI (https://github.com/unovue/reka-ui/issues/1885).
   * @default "false"
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value instead of the object itself.
   */
  valueKey?: VK | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  items?: T | undefined;
  /**
   * The value of the InputMenu when initially rendered. Use when you do not need to control the state of the InputMenu.
   */
  defaultValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C> | undefined;
  /**
   * The controlled value of the InputMenu. Can be binded-with with `v-model`.
   */
  modelValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C> | undefined;
  modelModifiers?: Mod | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: M | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  /**
   * When `true`, the input accepts free-form text with optional suggestions.
   * The `modelValue` becomes the input text (string) instead of a selected item.
   */
  autocomplete?: boolean | undefined;
  /**
   * Determines if custom user input that does not exist in options can be added.
   */
  createItem?: boolean | "always" | { position?: "top" | "bottom" | undefined; when?: "empty" | "always" | undefined; } | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
   */
  ignoreFilter?: boolean | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; trailingClear?: ClassNameValue; arrow?: ClassNameValue; content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; tagsItem?: ClassNameValue; tagsItemText?: ClassNameValue; tagsItemDelete?: ClassNameValue; tagsItemDeleteIcon?: ClassNameValue; tagsInput?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with listbox
   */
  disabled?: boolean | undefined;
  /**
   * The controlled open state of the Combobox. Can be binded with `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the combobox when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox input blurred
   * @default "true"
   */
  resetSearchTermOnBlur?: boolean | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox value is selected
   * @default "true"
   */
  resetSearchTermOnSelect?: boolean | undefined;
  /**
   * When `true` the `modelValue` will be reset to `null` (or `[]` if `multiple`)
   * @default "true"
   */
  resetModelValueOnClear?: boolean | undefined;
  /**
   * When `true`, hover over item will trigger highlight
   */
  highlightOnHover?: boolean | undefined;
  /**
   * Whether to open the combobox when the input is clicked
   */
  openOnClick?: boolean | undefined;
  /**
   * Whether to open the combobox when the input is focused
   */
  openOnFocus?: boolean | undefined;
  /**
   * Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
   */
  by?: string | ((a: T, b: T) => boolean) | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  enterKeyHint?: "search" | "enter" | "done" | "go" | "next" | "previous" | "send" | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  list?: string | undefined;
  readonly?: Booleanish | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
\> This component also supports all native \`\<input>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the InputMenu component
 */
interface InputMenuSlots {
  leading(): any;
  trailing(): any;
  empty(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  tags-item-text(): any;
  tags-item-delete(): any;
  content-top(): any;
  content-bottom(): any;
  create-item-label(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the InputMenu component
 */
interface InputMenuEmits {
  update:open: (payload: [value: boolean]) => void;
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  create: (payload: [item: string]) => void;
  clear: (payload: []) => void;
  highlight: (payload: [payload: { ref: HTMLElement; value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C>; } | undefined]) => void;
  remove-tag: (payload: [item: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C>]) => void;
  update:modelValue: (payload: [value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C>]) => void;
  update:searchTerm: (payload: [value: string]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                               |
| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    | `Ref<HTMLInputElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `viewportRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLDivElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}   |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    inputMenu: {
      slots: {
        root: 'relative inline-flex items-center',
        base: [
          'rounded-md',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'group absolute inset-y-0 end-0 flex items-center disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
        trailingIcon: 'shrink-0 text-dimmed',
        trailingClear: 'p-0',
        arrow: 'fill-bg stroke-default',
        content: 'max-h-60 w-(--reka-combobox-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-combobox-content-transform-origin) pointer-events-auto flex flex-col',
        viewport: 'relative scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        empty: 'text-center text-muted',
        label: 'font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: [
          'group relative w-full flex items-start gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
          'transition-colors before:transition-colors'
        ],
        itemLeadingIcon: [
          'shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
          'transition-colors'
        ],
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemLeadingChip: 'shrink-0',
        itemLeadingChipSize: '',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0',
        itemWrapper: 'flex-1 flex flex-col min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        tagsItem: 'px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-0.5 ring ring-inset ring-accented bg-elevated text-default data-disabled:cursor-not-allowed data-disabled:opacity-75',
        tagsItemText: 'truncate',
        tagsItemDelete: [
          'inline-flex items-center rounded-xs text-dimmed hover:text-default hover:bg-accented/75 disabled:pointer-events-none',
          'transition-colors'
        ],
        tagsItemDeleteIcon: 'shrink-0',
        tagsInput: 'flex-1 border-0 bg-transparent placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
      },
      variants: {
        fieldGroup: {
          horizontal: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none'
          },
          vertical: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none'
          }
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-sm/4 gap-1',
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            tagsItem: 'text-[10px]/3',
            tagsItemDeleteIcon: 'size-3',
            empty: 'p-2 text-xs'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-sm/4 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            tagsItem: 'text-[10px]/3',
            tagsItemDeleteIcon: 'size-3',
            empty: 'p-2.5 text-xs'
          },
          md: {
            base: 'px-2.5 py-1.5 text-base/5 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            tagsItem: 'text-xs',
            tagsItemDeleteIcon: 'size-3.5',
            empty: 'p-2.5 text-sm'
          },
          lg: {
            base: 'px-3 py-2 text-base/5 gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            tagsItem: 'text-xs',
            tagsItemDeleteIcon: 'size-3.5',
            empty: 'p-3 text-sm'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemLeadingChip: 'size-6',
            itemLeadingChipSize: 'lg',
            itemTrailingIcon: 'size-6',
            tagsItem: 'text-sm',
            tagsItemDeleteIcon: 'size-4',
            empty: 'p-3 text-base'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        },
        virtualize: {
          true: {
            viewport: 'p-1 isolate'
          },
          false: {
            viewport: 'divide-y divide-default'
          }
        },
        multiple: {
          true: {
            root: 'flex-wrap'
          },
          false: {
            base: 'w-full border-0 placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
          }
        }
      },
      compoundVariants: [
        {
          variant: 'soft',
          multiple: true,
          class: 'has-focus:bg-elevated'
        },
        {
          variant: 'ghost',
          multiple: true,
          class: 'has-focus:bg-elevated'
        },
        {
          color: 'primary',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-secondary'
        },
        {
          color: 'success',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-success'
        },
        {
          color: 'info',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-info'
        },
        {
          color: 'warning',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-warning'
        },
        {
          color: 'error',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-error'
        },
        {
          color: 'neutral',
          multiple: true,
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inverted'
        },
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/InputMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input-menu.ts).


# InputNumber

## Usage

Use the `v-model` directive to control the value of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UInputNumber :default-value="5" />
</template>
```

\> \[!NOTE]
\> This component relies on the \[\`@internationalized/number\`]\(https\://react-spectrum.adobe.com/internationalized/number/index.html) package which provides utilities for formatting and parsing numbers across locales and numbering systems.

### Min / Max

Use the `min` and `max` props to set the minimum and maximum values of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" :min="0" :max="10" />
</template>
```

### Step

Use the `step` prop to set the step value of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" :step="2" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" orientation="vertical" />
</template>
```

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<template>
  <UInputNumber placeholder="Enter a number" />
</template>
```

### Color

Use the `color` prop to change the ring color when the InputNumber is focused.

```vue
<template>
  <UInputNumber :model-value="5" color="neutral" highlight />
</template>
```

### Variant

Use the `variant` prop to change the variant of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" variant="subtle" color="neutral" :highlight="false" />
</template>
```

### Size

Use the `size` prop to change the size of the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" size="xl" />
</template>
```

### Disabled

Use the `disabled` prop to disable the InputNumber.

```vue
<template>
  <UInputNumber :model-value="5" disabled />
</template>
```

### Increment / Decrement

Use the `increment` and `decrement` props to customize the increment and decrement buttons with any [Button](https://ui.nuxt.com/docs/components/button) props. Defaults to `{ variant: 'link' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}.

```vue
<template>
  <UInputNumber :model-value="5" />
</template>
```

### Increment / Decrement Icons

Use the `increment-icon` and `decrement-icon` props to customize the buttons [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-plus` / `i-lucide-minus`.

```vue
<template>
  <UInputNumber :model-value="5" increment-icon="i-lucide-arrow-right" decrement-icon="i-lucide-arrow-left" />
</template>
```

## Examples

### With decimal format

Use the `format-options` prop to customize the format of the value.

```vue [InputNumberDecimalExample.vue]
<script setup lang="ts">
const value = ref(5)
</script>

<template>
  <UInputNumber
    v-model="value"
    :format-options="{
      signDisplay: 'exceptZero',
      minimumFractionDigits: 1
    }"
  />
</template>
```

### With percentage format

Use the `format-options` prop with `style: 'percent'` to customize the format of the value.

```vue [InputNumberPercentageExample.vue]
<script setup lang="ts">
const value = ref(0.05)
</script>

<template>
  <UInputNumber
    v-model="value"
    :step="0.01"
    :format-options="{
      style: 'percent'
    }"
  />
</template>
```

### With currency format

Use the `format-options` prop with `style: 'currency'` to customize the format of the value.

```vue [InputNumberCurrencyExample.vue]
<script setup lang="ts">
const value = ref(1500)
</script>

<template>
  <UInputNumber
    v-model="value"
    :format-options="{
      style: 'currency',
      currency: 'EUR',
      currencyDisplay: 'code',
      currencySign: 'accounting'
    }"
  />
</template>
```

### Without buttons

You can use the `increment` and `decrement` props to control visibility of the buttons.

```vue [InputNumberWithoutButtonsExample.vue]
<script setup lang="ts">
const value = ref(5)
</script>

<template>
  <UInputNumber
    v-model="value"
    :increment="false"
    :decrement="false"
  />
</template>
```

### Within a FormField

You can use the InputNumber within a [FormField](https://ui.nuxt.com/docs/components/form-field) component to display a label, help text, required indicator, etc.

```vue [InputNumberFormFieldExample.vue]
<script setup lang="ts">
const retries = ref(0)
</script>

<template>
  <UFormField label="Retries" help="Specify number of attempts" required>
    <UInputNumber v-model="retries" placeholder="Enter retries" />
  </UFormField>
</template>
```

### With slots

Use the `#increment` and `#decrement` slots to customize the buttons.

```vue [InputNumberSlotsExample.vue]
<script setup lang="ts">
const value = ref(5)
</script>

<template>
  <UInputNumber v-model="value">
    <template #decrement>
      <UButton size="xs" icon="i-lucide-minus" />
    </template>

    <template #increment>
      <UButton size="xs" icon="i-lucide-plus" />
    </template>
  </UInputNumber>
</template>
```

## API

### Props

```ts
/**
 * Props for the InputNumber component
 */
interface InputNumberProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The placeholder text when the input is empty.
   */
  placeholder?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  /**
   * The orientation of the input number.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * Configure the increment button. The `color` and `size` are inherited.
   * @default "true"
   */
  increment?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed to increment the value.
   */
  incrementIcon?: any;
  /**
   * Disable the increment button.
   */
  incrementDisabled?: boolean | undefined;
  /**
   * Configure the decrement button. The `color` and `size` are inherited.
   * @default "true"
   */
  decrement?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed to decrement the value.
   */
  decrementIcon?: any;
  /**
   * Disable the decrement button.
   */
  decrementDisabled?: boolean | undefined;
  autofocus?: boolean | undefined;
  autofocusDelay?: number | undefined;
  defaultValue?: NonNullable<T> | undefined;
  modelValue?: ApplyModifiers<T, Mod> | undefined;
  modelModifiers?: Mod | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; increment?: ClassNameValue; decrement?: ClassNameValue; } | undefined;
  /**
   * The smallest value allowed for the input.
   */
  min?: number | undefined;
  /**
   * The largest value allowed for the input.
   */
  max?: number | undefined;
  /**
   * The amount that the input value changes with each increment or decrement "tick".
   */
  step?: number | undefined;
  /**
   * When `false`, prevents the value from snapping to the nearest increment of the step value
   */
  stepSnapping?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with the Number Field.
   */
  disabled?: boolean | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * Id of the element
   */
  id?: string | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * Formatting options for the value displayed in the number field. This also affects what characters are allowed to be typed by the user.
   */
  formatOptions?: Intl.NumberFormatOptions | undefined;
  /**
   * When `true`, prevents the value from changing on wheel scroll.
   */
  disableWheelChange?: boolean | undefined;
  /**
   * When `true`, inverts the direction of the wheel change.
   */
  invertWheelChange?: boolean | undefined;
  /**
   * When `true`, the Number Field is read-only.
   */
  readonly?: boolean | undefined;
  /**
   * When `true`, the input will be focused when the value changes.
   */
  focusOnChange?: boolean | undefined;
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  list?: string | undefined;
  autocomplete?: "on" | "off" | (string & {}) | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
\> This component also supports all native \`\<input>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the InputNumber component
 */
interface InputNumberSlots {
  increment(): any;
  decrement(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the InputNumber component
 */
interface InputNumberEmits {
  update:modelValue: (payload: [value: ApplyModifiers<T, Mod>]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  change: (payload: [event: Event]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                           | Type                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLInputElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    inputNumber: {
      slots: {
        root: 'relative inline-flex items-center',
        base: [
          'w-full rounded-md border-0 placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        increment: 'absolute flex items-center',
        decrement: 'absolute flex items-center'
      },
      variants: {
        fieldGroup: {
          horizontal: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none'
          },
          vertical: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none'
          }
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        size: {
          xs: 'px-2 py-1 text-sm/4 gap-1',
          sm: 'px-2.5 py-1.5 text-sm/4 gap-1.5',
          md: 'px-2.5 py-1.5 text-base/5 gap-1.5',
          lg: 'px-3 py-2 text-base/5 gap-2',
          xl: 'px-3 py-2 text-base gap-2'
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        disabled: {
          true: {
            increment: 'opacity-75 cursor-not-allowed',
            decrement: 'opacity-75 cursor-not-allowed'
          }
        },
        orientation: {
          horizontal: {
            base: 'text-center',
            increment: 'inset-y-0 end-0 pe-1',
            decrement: 'inset-y-0 start-0 ps-1'
          },
          vertical: {
            increment: 'top-0 end-0 pe-1 [&>button]:py-0 scale-80',
            decrement: 'bottom-0 end-0 pe-1 [&>button]:py-0 scale-80'
          }
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        increment: {
          false: ''
        },
        decrement: {
          false: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          orientation: 'horizontal',
          decrement: false,
          class: 'text-start'
        },
        {
          decrement: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          decrement: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          decrement: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          decrement: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          decrement: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          increment: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          increment: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          increment: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          increment: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          increment: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/InputNumber.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input-number.ts).


# InputTags

## Usage

Use the `v-model` directive to control the value of the InputTags.

```vue
<template>
  <UInputTags />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UInputTags />
</template>
```

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<template>
  <UInputTags placeholder="Enter tags..." />
</template>
```

### Max Length

Use the `max-length` prop to set the maximum number of characters allowed in a tag.

```vue
<template>
  <UInputTags :max-length="4" />
</template>
```

### Color

Use the `color` prop to change the ring color when the InputTags is focused.

```vue
<template>
  <UInputTags color="neutral" highlight />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variants

Use the `variant` prop to change the appearance of the InputTags.

```vue
<template>
  <UInputTags variant="subtle" color="neutral" :highlight="false" />
</template>
```

### Sizes

Use the `size` prop to adjust the size of the InputTags.

```vue
<template>
  <UInputTags size="xl" />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the InputTags.

```vue
<template>
  <UInputTags icon="i-lucide-search" size="md" variant="outline" />
</template>
```

\> \[!NOTE]
\> Use the \`leading\` and \`trailing\` props to set the icon position or the \`leading-icon\` and \`trailing-icon\` props to set a different icon for each position.

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the InputTags.

```vue
<template>
  <UInputTags size="md" variant="outline" />
</template>
```

### Delete Icon

Use the `delete-icon` prop to customize the delete [Icon](https://ui.nuxt.com/docs/components/icon) in the tags. Defaults to `i-lucide-x`.

```vue
<template>
  <UInputTags delete-icon="i-lucide-trash" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Loading

Use the `loading` prop to show a loading icon on the InputTags.

```vue
<template>
  <UInputTags loading :trailing="false" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <UInputTags loading loading-icon="i-lucide-loader" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the InputTags.

```vue
<template>
  <UInputTags disabled />
</template>
```

## Examples

### Within a FormField

You can use the InputTags within a [FormField](https://ui.nuxt.com/docs/components/form-field) component to display a label, help text, required indicator, etc.

```vue [InputTagsFormFieldExample.vue]
<script setup lang="ts">
const tags = ref(['Vue'])
</script>

<template>
  <UFormField label="Tags" required>
    <UInputTags v-model="tags" placeholder="Enter tags..." />
  </UFormField>
</template>
```

## API

### Props

```ts
/**
 * Props for the InputTags component
 */
interface InputTagsProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The placeholder text when the input is empty.
   */
  placeholder?: string | undefined;
  /**
   * The maximum number of character allowed.
   */
  maxLength?: number | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  /**
   * The icon displayed to delete a tag.
   */
  deleteIcon?: any;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; item?: ClassNameValue; itemText?: ClassNameValue; itemDelete?: ClassNameValue; itemDeleteIcon?: ClassNameValue; input?: ClassNameValue; } | undefined;
  /**
   * The controlled value of the tags input. Can be bind as `v-model`.
   */
  modelValue?: T[] | null | undefined;
  /**
   * The value of the tags that should be added. Use when you do not need to control the state of the tags input
   */
  defaultValue?: T[] | undefined;
  /**
   * When `true`, allow adding tags on paste. Work in conjunction with delimiter prop.
   */
  addOnPaste?: boolean | undefined;
  /**
   * When `true` allow adding tags on tab keydown
   */
  addOnTab?: boolean | undefined;
  /**
   * When `true` allow adding tags blur input
   */
  addOnBlur?: boolean | undefined;
  /**
   * When `true`, allow duplicated tags.
   */
  duplicate?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with the tags input.
   */
  disabled?: boolean | undefined;
  /**
   * The character or regular expression to trigger the addition of a new tag. Also used to split tags for `@paste` event
   */
  delimiter?: string | RegExp | undefined;
  /**
   * Maximum number of tags.
   */
  max?: number | undefined;
  id?: string | undefined;
  /**
   * Convert the input value to the desired type. Mandatory when using objects as values and using `TagsInputInput`
   */
  convertValue?: ((value: string) => T) | undefined;
  /**
   * Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix or when using object as values
   */
  displayValue?: ((value: T) => string) | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  list?: string | undefined;
  readonly?: Booleanish | undefined;
  autocomplete?: "on" | "off" | (string & {}) | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
\> This component also supports all native \`\<input>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the InputTags component
 */
interface InputTagsSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  item-text(): any;
  item-delete(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the InputTags component
 */
interface InputTagsEmits {
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  update:modelValue: (payload: [payload: T[]]) => void;
  invalid: (payload: [payload: T]) => void;
  addTag: (payload: [payload: T]) => void;
  removeTag: (payload: [payload: T]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                           | Type                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLInputElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    inputTags: {
      slots: {
        root: [
          'relative inline-flex items-center',
          'flex-wrap'
        ],
        base: [
          'rounded-md',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed',
        item: 'px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-0.5 ring ring-inset ring-accented bg-elevated text-default data-disabled:cursor-not-allowed data-disabled:opacity-75 wrap-anywhere data-[state="active"]:bg-accented',
        itemText: '',
        itemDelete: [
          'inline-flex items-center rounded-xs text-dimmed hover:text-default hover:bg-accented/75 disabled:pointer-events-none',
          'transition-colors'
        ],
        itemDeleteIcon: 'shrink-0',
        input: 'flex-1 border-0 bg-transparent placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
      },
      variants: {
        fieldGroup: {
          horizontal: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none'
          },
          vertical: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none'
          }
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-sm/4 gap-1',
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            item: 'text-[10px]/3',
            itemDeleteIcon: 'size-3'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-sm/4 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            item: 'text-[10px]/3',
            itemDeleteIcon: 'size-3'
          },
          md: {
            base: 'px-2.5 py-1.5 text-base/5 gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            item: 'text-xs',
            itemDeleteIcon: 'size-3.5'
          },
          lg: {
            base: 'px-3 py-2 text-base/5 gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            item: 'text-xs',
            itemDeleteIcon: 'size-3.5'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            item: 'text-sm',
            itemDeleteIcon: 'size-4'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated has-focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated has-focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/InputTags.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input-tags.ts).


# InputTime

## Usage

Use the `v-model` directive to control the selected date.

```vue
<template>
  <UInputTime />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UInputTime />
</template>
```

\*\*Nuxt:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The time format is determined by the \`locale\` prop of the App component.
\*\*Vue:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/vue#locale
\> This component uses the \`@internationalized/date\` package for locale-aware formatting. The time format is determined by the \`locale\` prop of the App component.

### Hour Cycle

Use the `hour-cycle` prop to change the hour cycle of the InputTime. Defaults to `12`.

```vue
<template>
  <UInputTime :hour-cycle="24" />
</template>
```

### Color

Use the `color` prop to change the color of the InputTime.

```vue
<template>
  <UInputTime color="neutral" highlight />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the InputTime.

```vue
<template>
  <UInputTime variant="subtle" />
</template>
```

### Size

Use the `size` prop to change the size of the InputTime.

```vue
<template>
  <UInputTime size="xl" />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the InputTime.

```vue
<template>
  <UInputTime icon="i-lucide-clock" />
</template>
```

\> \[!NOTE]
\> Use the \`leading\` and \`trailing\` props to set the icon position or the \`leading-icon\` and \`trailing-icon\` props to set a different icon for each position.

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the InputTime.

```vue
<template>
  <UInputTime size="md" variant="outline" />
</template>
```

### Disabled

Use the `disabled` prop to disable the InputTime.

```vue
<template>
  <UInputTime disabled />
</template>
```

## Examples

### Within a FormField

You can use the InputTime within a [FormField](https://ui.nuxt.com/docs/components/form-field) component to display a label, help text, required indicator, etc.

```vue [InputTimeFormFieldExample.vue]
<script setup lang="ts">
import { Time } from '@internationalized/date'

const time = shallowRef(new Time(12, 30, 0))
</script>

<template>
  <UFormField label="Time" help="Specify the time" required>
    <UInputTime v-model="time" />
  </UFormField>
</template>
```

## API

### Props

```ts
/**
 * Props for the InputTime component
 */
interface InputTimeProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; segment?: ClassNameValue; } | undefined;
  defaultValue?: Time | CalendarDateTime | ZonedDateTime;
  defaultPlaceholder?: Time | CalendarDateTime | ZonedDateTime;
  placeholder?: Time | CalendarDateTime | ZonedDateTime;
  modelValue?: null | Time | CalendarDateTime | ZonedDateTime;
  /**
   * The hour cycle used for formatting times. Defaults to the local preference
   */
  hourCycle?: HourCycle;
  /**
   * The stepping interval for the time fields. Defaults to `1`.
   */
  step?: DateStep | undefined;
  /**
   * Whether to enforce snapping the value to the nearest step increment after input. Defaults to `false`.
   */
  stepSnapping?: boolean | undefined;
  /**
   * The granularity to use for formatting times. Defaults to minute if a Time is provided, otherwise defaults to minute. The field will render segments for each part of the date up to and including the specified granularity
   */
  granularity?: "hour" | "minute" | "second" | undefined;
  /**
   * Whether or not to hide the time zone segment of the field
   */
  hideTimeZone?: boolean | undefined;
  maxValue?: Time | CalendarDateTime | ZonedDateTime;
  minValue?: Time | CalendarDateTime | ZonedDateTime;
  /**
   * Whether or not the time field is disabled
   */
  disabled?: boolean | undefined;
  /**
   * Whether or not the time field is readonly
   */
  readonly?: boolean | undefined;
  /**
   * Id of the element
   */
  id?: string | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

### Slots

```ts
/**
 * Slots for the InputTime component
 */
interface InputTimeSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the InputTime component
 */
interface InputTimeEmits {
  blur: (payload: [event: FocusEvent]) => void;
  change: (payload: [event: Event]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  update:modelValue: (payload: [date: TimeValue | undefined]) => void;
  update:placeholder: (payload: [date: TimeValue]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    inputTime: {
      slots: {
        base: [
          'group relative inline-flex items-center rounded-md select-none',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed',
        segment: [
          'rounded text-center outline-hidden data-placeholder:text-dimmed data-[segment=literal]:text-muted data-invalid:text-error data-disabled:cursor-not-allowed data-disabled:opacity-75',
          'transition-colors'
        ]
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        size: {
          xs: {
            base: [
              'px-2 py-1 text-sm/4 gap-1',
              'gap-0.25'
            ],
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            segment: 'not-data-[segment=literal]:w-6'
          },
          sm: {
            base: [
              'px-2.5 py-1.5 text-sm/4 gap-1.5',
              'gap-0.5'
            ],
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            segment: 'not-data-[segment=literal]:w-6'
          },
          md: {
            base: [
              'px-2.5 py-1.5 text-base/5 gap-1.5',
              'gap-0.5'
            ],
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            segment: 'not-data-[segment=literal]:w-7'
          },
          lg: {
            base: [
              'px-3 py-2 text-base/5 gap-2',
              'gap-0.75'
            ],
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            segment: 'not-data-[segment=literal]:w-7'
          },
          xl: {
            base: [
              'px-3 py-2 text-base gap-2',
              'gap-0.75'
            ],
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            segment: 'not-data-[segment=literal]:w-8'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        }
      },
      compoundVariants: [
        {
          variant: 'outline',
          class: {
            segment: 'focus:bg-elevated'
          }
        },
        {
          variant: 'soft',
          class: {
            segment: 'focus:bg-accented/50 group-hover:focus:bg-accented'
          }
        },
        {
          variant: 'subtle',
          class: {
            segment: 'focus:bg-accented'
          }
        },
        {
          variant: 'ghost',
          class: {
            segment: 'focus:bg-elevated group-hover:focus:bg-accented'
          }
        },
        {
          variant: 'none',
          class: {
            segment: 'focus:bg-elevated'
          }
        },
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/InputTime.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/input-time.ts).


# Kbd

## Usage

Use the default slot to set the value of the Kbd.

```vue
<template>
  <UKbd>
    K
  </UKbd>
</template>
```

### Value

Use the `value` prop to set the value of the Kbd.

```vue
<template>
  <UKbd value="K" />
</template>
```

You can pass special keys to the `value` prop that goes through the [`useKbd`](https://github.com/nuxt/ui/blob/v4/src/runtime/composables/useKbd.ts){rel="&#x22;nofollow&#x22;"} composable. For example, the `meta` key displays as `⌘` on macOS and `Ctrl` on other platforms.

```vue
<template>
  <UKbd value="meta" />
</template>
```

### Color

Use the `color` prop to change the color of the Kbd.

```vue
<template>
  <UKbd color="neutral">
    K
  </UKbd>
</template>
```

### Variant

Use the `variant` prop to change the variant of the Kbd.

```vue
<template>
  <UKbd color="neutral" variant="solid">
    K
  </UKbd>
</template>
```

### Size

Use the `size` prop to change the size of the Kbd.

```vue
<template>
  <UKbd size="lg">
    K
  </UKbd>
</template>
```

## Examples

### `class` prop

Use the `class` prop to override the base styles of the Badge.

```vue
<template>
  <UKbd class="font-bold rounded-full" variant="subtle">
    K
  </UKbd>
</template>
```

## API

### Props

```ts
/**
 * Props for the Kbd component
 */
interface KbdProps {
  /**
   * The element or component this component should render as.
   * @default "\"kbd\""
   */
  as?: any;
  value?: string | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "solid" | undefined;
  size?: "sm" | "md" | "lg" | undefined;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Kbd component
 */
interface KbdSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    kbd: {
      base: 'inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans uppercase',
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        size: {
          sm: 'h-4 min-w-[16px] text-[10px]',
          md: 'h-5 min-w-[20px] text-[11px]',
          lg: 'h-6 min-w-[24px] text-[12px]'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: 'text-inverted bg-primary'
        },
        {
          color: 'secondary',
          variant: 'solid',
          class: 'text-inverted bg-secondary'
        },
        {
          color: 'success',
          variant: 'solid',
          class: 'text-inverted bg-success'
        },
        {
          color: 'info',
          variant: 'solid',
          class: 'text-inverted bg-info'
        },
        {
          color: 'warning',
          variant: 'solid',
          class: 'text-inverted bg-warning'
        },
        {
          color: 'error',
          variant: 'solid',
          class: 'text-inverted bg-error'
        },
        {
          color: 'primary',
          variant: 'outline',
          class: 'ring ring-inset ring-primary/50 text-primary'
        },
        {
          color: 'secondary',
          variant: 'outline',
          class: 'ring ring-inset ring-secondary/50 text-secondary'
        },
        {
          color: 'success',
          variant: 'outline',
          class: 'ring ring-inset ring-success/50 text-success'
        },
        {
          color: 'info',
          variant: 'outline',
          class: 'ring ring-inset ring-info/50 text-info'
        },
        {
          color: 'warning',
          variant: 'outline',
          class: 'ring ring-inset ring-warning/50 text-warning'
        },
        {
          color: 'error',
          variant: 'outline',
          class: 'ring ring-inset ring-error/50 text-error'
        },
        {
          color: 'primary',
          variant: 'soft',
          class: 'text-primary bg-primary/10'
        },
        {
          color: 'secondary',
          variant: 'soft',
          class: 'text-secondary bg-secondary/10'
        },
        {
          color: 'success',
          variant: 'soft',
          class: 'text-success bg-success/10'
        },
        {
          color: 'info',
          variant: 'soft',
          class: 'text-info bg-info/10'
        },
        {
          color: 'warning',
          variant: 'soft',
          class: 'text-warning bg-warning/10'
        },
        {
          color: 'error',
          variant: 'soft',
          class: 'text-error bg-error/10'
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: 'text-primary ring ring-inset ring-primary/25 bg-primary/10'
        },
        {
          color: 'secondary',
          variant: 'subtle',
          class: 'text-secondary ring ring-inset ring-secondary/25 bg-secondary/10'
        },
        {
          color: 'success',
          variant: 'subtle',
          class: 'text-success ring ring-inset ring-success/25 bg-success/10'
        },
        {
          color: 'info',
          variant: 'subtle',
          class: 'text-info ring ring-inset ring-info/25 bg-info/10'
        },
        {
          color: 'warning',
          variant: 'subtle',
          class: 'text-warning ring ring-inset ring-warning/25 bg-warning/10'
        },
        {
          color: 'error',
          variant: 'subtle',
          class: 'text-error ring ring-inset ring-error/25 bg-error/10'
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: 'text-inverted bg-inverted'
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: 'ring ring-inset ring-accented text-default bg-default'
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: 'text-default bg-elevated'
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: 'ring ring-inset ring-accented text-default bg-elevated'
        }
      ],
      defaultVariants: {
        variant: 'outline',
        color: 'neutral',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Kbd.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/kbd.ts).


# Link

## Usage

The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} using the [`custom`](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom){rel="&#x22;nofollow&#x22;"} prop. It provides a few extra props:

- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
- `exact-query` and `exact-hash` props to style with `active-class`when the link is active and the query or hash is exactly the same as the current query or hash.
  - use `exact-query="partial"` to style with `active-class` when the link is active and the query partially match the current query.

The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link){rel="&#x22;nofollow&#x22;"} guide.

\> \[!NOTE]
\> It is used by the \[\`Breadcrumb\`]\(/docs/components/breadcrumb), \[\`Button\`]\(/docs/components/button), \[\`ContextMenu\`]\(/docs/components/context-menu), \[\`DropdownMenu\`]\(/docs/components/dropdown-menu) and \[\`NavigationMenu\`]\(/docs/components/navigation-menu) components.

### Tag

The `Link` components renders an `<a>` tag when a `to` prop is provided, otherwise it renders a `<button>` tag. You can use the `as` prop to change fallback tag.

```vue
<template>
  <ULink to="" as="button">
    Link
  </ULink>
</template>
```

\> \[!NOTE]
\> You can inspect the rendered HTML by changing the \`to\` prop.

### Style

By default, the link has default active and inactive styles, check out the [#theme](https://ui.nuxt.com/#theme) section.

```vue
<template>
  <ULink to="/docs/components/link">
    Link
  </ULink>
</template>
```

\> \[!NOTE]
\> Try changing the \`to\` prop to see the active and inactive states.

You can override this behavior by using the `raw` prop and provide your own styles using `class`, `active-class` and `inactive-class`.

```vue
<template>
  <ULink raw to="/docs/components/link" active-class="font-bold" inactive-class="text-muted">
    Link
  </ULink>
</template>
```

## IntelliSense

If you're using VSCode and wish to get autocompletion for the classes `active-class` and `inactive-class`, you can add the following settings to your `.vscode/settings.json`:

```json [.vscode/settings.json]
{
  "tailwindCSS.classAttributes": [
    "active-class",
    "inactive-class"
  ]
}
```

## API

### Props

```ts
/**
 * Props for the Link component
 */
interface LinkProps {
  /**
   * The element or component this component should render as when not a link.
   * @default "\"button\""
   */
  as?: any;
  /**
   * The type of the button when not a link.
   * @default "\"button\""
   */
  type?: "reset" | "submit" | "button" | undefined;
  disabled?: boolean | undefined;
  /**
   * Force the link to be active independent of the current route.
   * @default "undefined"
   */
  active?: boolean | undefined;
  /**
   * Will only be active if the current route is an exact match.
   */
  exact?: boolean | undefined;
  /**
   * Allows controlling how the current route query sets the link as active.
   */
  exactQuery?: boolean | "partial" | undefined;
  /**
   * Will only be active if the current route hash is an exact match.
   */
  exactHash?: boolean | undefined;
  /**
   * The class to apply when the link is inactive.
   */
  inactiveClass?: string | undefined;
  /**
   * Whether RouterLink should not wrap its content in an `a` tag. Useful when
   * using `v-slot` to create a custom RouterLink
   */
  custom?: boolean | undefined;
  /**
   * When `true`, only styles from `class`, `activeClass`, and `inactiveClass` will be applied.
   */
  raw?: boolean | undefined;
  /**
   * Route Location the link should navigate to when clicked on.
   */
  to?: string | St | vt | undefined;
  /**
   * An alias for `to`. If used with `to`, `href` will be ignored
   */
  href?: string | St | vt | undefined;
  /**
   * Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
   */
  external?: boolean | undefined;
  /**
   * Where to display the linked URL, as the name for a browsing context.
   */
  target?: (string & {}) | "_blank" | "_parent" | "_self" | "_top" | null | undefined;
  /**
   * A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
   */
  rel?: "noopener" | "noreferrer" | "nofollow" | "sponsored" | "ugc" | (string & {}) | null | undefined;
  /**
   * If set to true, no rel attribute will be added to the link
   */
  noRel?: boolean | undefined;
  /**
   * A class to apply to links that have been prefetched.
   */
  prefetchedClass?: string | undefined;
  /**
   * When enabled will prefetch middleware, layouts and payloads of links in the viewport.
   */
  prefetch?: boolean | undefined;
  /**
   * Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility.
   */
  prefetchOn?: "visibility" | "interaction" | Partial<{ visibility: boolean; interaction: boolean; }> | undefined;
  /**
   * Escape hatch to disable `prefetch` attribute.
   */
  noPrefetch?: boolean | undefined;
  /**
   * An option to either add or remove trailing slashes in the `href` for this specific link.
   * Overrides the global `trailingSlash` option if provided.
   */
  trailingSlash?: "remove" | "append" | undefined;
  /**
   * Class to apply when the link is active
   */
  activeClass?: string | undefined;
  /**
   * Class to apply when the link is exact active
   */
  exactActiveClass?: string | undefined;
  /**
   * Value passed to the attribute `aria-current` when the link is exact active.
   * @default "\"page\""
   */
  ariaCurrentValue?: "step" | "page" | "true" | "false" | "location" | "date" | "time" | undefined;
  /**
   * Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported.
   */
  viewTransition?: boolean | undefined;
  /**
   * Calls `router.replace` instead of `router.push`.
   */
  replace?: boolean | undefined;
  name?: string | undefined;
  autofocus?: Booleanish | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
  download?: any;
  hreflang?: string | undefined;
  media?: string | undefined;
  ping?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attributes
\> This component also supports all native \`\<a>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Link component
 */
interface LinkSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    link: {
      base: 'focus-visible:outline-primary',
      variants: {
        active: {
          true: 'text-primary',
          false: 'text-muted'
        },
        disabled: {
          true: 'cursor-not-allowed opacity-75'
        }
      },
      compoundVariants: [
        {
          active: false,
          disabled: false,
          class: [
            'hover:text-default',
            'transition-colors'
          ]
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Link.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/link.ts).


# LocaleSelect

## Usage

The LocaleSelect component extends the [SelectMenu](https://ui.nuxt.com/docs/components/select-menu) component, so you can pass any property such as `color`, `variant`, `size`, etc.

\*\*Nuxt:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/nuxt
\> This component is meant to be used with the i18n system. Learn more about it in the guide.
\*\*Vue:\*\*
\> \[!NOTE]
\> See: /docs/getting-started/integrations/i18n/vue
\> This component is meant to be used with the i18n system. Learn more about it in the guide.

\> \[!WARNING]
\> The flags are displayed using Unicode characters. This may result in a different display, e.g. Microsoft Edge under Windows displays the ISO 3166-1 alpha-2 code instead, as no flag icons are shipped with the OS fonts.

### Locales

Use the `locales` prop with an array of locales from `@nuxt/ui/locale`.

```vue [LocaleSelectExample.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'

const locale = ref('en')
</script>

<template>
  <ULocaleSelect v-model="locale" :locales="Object.values(locales)" class="w-48" />
</template>
```

You can pass only the locales you need in your application:

```vue
<script setup lang="ts">
import { en, es, fr } from '@nuxt/ui/locale'

const locale = ref('en')
</script>

<template>
  <ULocaleSelect v-model="locale" :locales="[en, es, fr]" />
</template>
```

### Dynamic locale

\*\*Nuxt:\*\*
You can use it with Nuxt i18n:
\`\`\`vue
\<script setup lang="ts">
import \* as locales from '@nuxt/ui/locale'
const { locale, setLocale } = useI18n()
\</script>
\<template>
\<ULocaleSelect
\:model-value="locale"
\:locales="Object.values(locales)"
@update\:model-value="setLocale($event)"
/>
\</template>
\`\`\`
\*\*Vue:\*\*
You can use it with Vue i18n:
\`\`\`vue
\<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import \* as locales from '@nuxt/ui/locale'
const { locale, setLocale } = useI18n()
\</script>
\<template>
\<ULocaleSelect
\:model-value="locale"
\:locales="Object.values(locales)"
@update\:model-value="setLocale($event)"
/>
\</template>
\`\`\`

## API

### Props

```ts
/**
 * Props for the LocaleSelect component
 */
interface LocaleSelectProps {
  modelValue: string;
  locales?: Locale<any>[] | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; value?: ClassNameValue; placeholder?: ClassNameValue; arrow?: ClassNameValue; content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; input?: ClassNameValue; focusScope?: ClassNameValue; trailingClear?: ClassNameValue; } | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  autofocus?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with listbox
   */
  disabled?: boolean | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  /**
   * The controlled open state of the Combobox. Can be binded with `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the combobox when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox input blurred
   */
  resetSearchTermOnBlur?: boolean | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox value is selected
   */
  resetSearchTermOnSelect?: boolean | undefined;
  /**
   * When `true` the `modelValue` will be reset to `null` (or `[]` if `multiple`)
   */
  resetModelValueOnClear?: boolean | undefined;
  /**
   * When `true`, hover over item will trigger highlight
   */
  highlightOnHover?: boolean | undefined;
  /**
   * Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
   */
  by?: string | ((a: Locale<any>[], b: Locale<any>[]) => boolean) | undefined;
  /**
   * The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu.
   */
  defaultValue?: string | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: false | undefined;
  required?: boolean | undefined;
  id?: string | undefined;
  /**
   * The placeholder text when the select is empty.
   */
  placeholder?: string | undefined;
  /**
   * Whether to display the search input or not.
   * Can be an object to pass additional props to the input.
   * `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
   * @default "false"
   */
  searchInput?: boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * Display a clear button to reset the model value.
   * Can be an object to pass additional props to the Button.
   */
  clear?: false | (false & Partial<Omit<ButtonProps, LinkPropsKeys>>) | undefined;
  /**
   * The icon displayed in the clear button.
   */
  clearIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<ComboboxContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DismissableLayerEmits>>) | undefined;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<ComboboxArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the menu in a portal.
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, all groups are flattened into a single list due to a limitation of Reka UI (https://github.com/unovue/reka-ui/issues/1885).
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value instead of the object itself.
   * @default "\"code\""
   */
  valueKey?: "code" | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"name\""
   */
  labelKey?: GetItemKeys<Locale<any>[]> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   */
  descriptionKey?: GetItemKeys<Locale<any>[]> | undefined;
  modelModifiers?: Omit<ModelModifiers, "lazy"> | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Determines if custom user input that does not exist in options can be added.
   */
  createItem?: boolean | "always" | { position?: "top" | "bottom" | undefined; when?: "empty" | "always" | undefined; } | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
   */
  ignoreFilter?: boolean | undefined;
  autofocusDelay?: number | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/locale/LocaleSelect.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/locale/locale-select.ts).


# Main

## Usage

The Main component renders a `<main>` element that works together with the [Header](https://ui.nuxt.com/docs/components/header) component to create a full-height layout that extends to the viewport's available height.

\> \[!TIP]
\> See: /docs/getting-started/theme/css-variables#header
\> The Main component uses the \`--ui-header-height\` CSS variable to position itself correctly below the \`Header\`.

## Examples

### Within `app.vue`

Use the Main component in your `app.vue` or in a layout:

```vue [app.vue] {5-9}
<template>
  <UApp>
    <UHeader />

    <UMain>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </UMain>

    <UFooter />
  </UApp>
</template>
```

## API

### Props

```ts
/**
 * Props for the Main component
 */
interface MainProps {
  /**
   * The element or component this component should render as.
   * @default "\"main\""
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Main component
 */
interface MainSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    main: {
      base: 'min-h-[calc(100vh-var(--ui-header-height))]'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Main.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/main.ts).


# Marquee

## Usage

Use the default slot with your content to create an infinite scrolling animation.

```vue
<template>
  <UMarquee>
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

### Pause on Hover

Use the `pause-on-hover` prop to pause the animation when the user hovers over the content.

```vue
<template>
  <UMarquee pause-on-hover>
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

### Reverse

Use the `reverse` prop to reverse the direction of the animation.

```vue
<template>
  <UMarquee reverse>
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

### Orientation

Use the `orientation` prop to change the scrolling direction.

```vue
<template>
  <UMarquee orientation="vertical">
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

### Repeat

Use the `repeat` prop to specify how many times the content should be repeated in the animation.

```vue
<template>
  <UMarquee :repeat="6">
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

### Overlay

Use the `overlay` prop to remove the gradient overlays on the edges of the marquee.

```vue
<template>
  <UMarquee :overlay="false">
    <UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
    <UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
  </UMarquee>
</template>
```

## Examples

### Testimonials

Use the `Marquee` component to create an infinite scrolling animation for your testimonials.

```vue [MarqueeTestimonials.vue]
<script setup lang="ts">
import type { UserProps } from '@nuxt/ui'

const testimonials: { user: UserProps, quote: string }[] = [{
  user: {
    name: 'Anthony Bettini',
    description: 'CEO and founder of VulnCheck',
    avatar: {
      src: 'https://media.licdn.com/dms/image/v2/C4E03AQEY3pmXsH8hDg/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1519741249442?e=1746057600&v=beta&t=dvQfBT9ah03MPNy9cnly30ugreeCdxG4nrxV3lwKAC8',
      loading: 'lazy' as const
    }
  },
  quote: 'We were using a SaaS service for the docs site, but were left unfulfilled. We put in the effort to do it in house, with UI Pro and not only did we get complimented by a prospect on our site, but they wanted to know our platform.'
}, {
  user: {
    name: 'Yaz Jallad',
    description: 'Founder Ninjaparade Digital',
    avatar: {
      src: 'https://pbs.twimg.com/profile_images/1824690890222485504/lQ7v1AGt_400x400.jpg',
      loading: 'lazy' as const
    }
  },
  quote: 'Wow, Nuxt UI Pro is a total game-changer! I\'m seriously impressed with the quality, attention to detail, and the insane variety of components you get. It\'s like hitting the jackpot for any developer. I\'ve saved countless hours that I would\'ve spent stressing over making my apps look good, with amazing accessible UX,  and instead, I\'ve been able to focus on the real deal – building the app itself. It\'s an instant buy for me, every single time. No second thoughts!'
}, {
  user: {
    name: 'Kevin Olson',
    description: 'Founder of Fume.app',
    avatar: {
      src: 'https://ipx.nuxt.com/f_auto,s_40x40/gh_avatar/acidjazz',
      srcset: 'https://ipx.nuxt.com/f_auto,s_80x80/gh_avatar/acidjazz 2x',
      loading: 'lazy' as const
    }
  },
  quote: 'Nuxt UI Pro saves 100s of hours of dev and design time while delivering a clean professional look on any device.'
}, {
  user: {
    name: 'Michael Hoffmann',
    description: 'Senior Frontend Developer',
    avatar: {
      src: 'https://ipx.nuxt.com/f_auto,s_40x40/gh_avatar/mokkapps',
      srcset: 'https://ipx.nuxt.com/f_auto,s_80x80/gh_avatar/mokkapps 2x',
      loading: 'lazy' as const
    }
  },
  quote: 'I decided to replace my custom-built components with a component library and chose Nuxt UI Pro. It only took me a few hours, and the new UI looks more professional. Integrating the library is easy; the components are well-documented and highly customizable. I can only recommend it; this library is my new choice for new SaaS products.'
}, {
  user: {
    name: 'Harlan Wilton',
    description: 'Nuxt core team member',
    avatar: {
      src: 'https://ipx.nuxt.com/f_auto,s_40x40/gh_avatar/harlan-zw',
      srcset: 'https://ipx.nuxt.com/f_auto,s_80x80/gh_avatar/harlan-zw 2x',
      loading: 'lazy' as const
    }
  },
  quote: 'Nuxt UI Pro is my go to component library. Out-of-the-box it handles all of the UI demands I throw at it while looking great. The customisation is really worth thought out, allowing you to override components in a breeze. Always amazed at the improvements dropped in each update as well, the team is doing an amazing job.'
}, {
  user: {
    name: 'Thomas Sanlis',
    description: 'Freelance developer and designer',
    avatar: {
      src: 'https://pbs.twimg.com/profile_images/1374040164180299791/ACw4G3nZ_400x400.jpg',
      loading: 'lazy' as const
    }
  },
  quote: 'I jumped at the chance to buy the Nuxt team\'s new UI kit as soon as I saw it. While I\'m already a fan of Nuxt UI, the pro version takes it to a whole new level and lets me paste entire blocks into all my projects, saving me a ton of time.'
}, {
  user: {
    name: 'Benjamin Code',
    description: 'YouTuber and SaaS builder',
    avatar: {
      src: 'https://pbs.twimg.com/profile_images/1607353032420769793/I8qQSUfQ_400x400.jpg',
      loading: 'lazy' as const
    }
  },
  quote: 'Nuxt UI has allowed me to develop my SaaS without any prior mockups. The design quality of their components and the intelligence of the DX meant that I was able to try many different layouts for my application until I found the perfect UX for my users. Nuxt UI is the ui-kit I would have dreamed of building myself, and Nuxt UI Pro makes things even easier when you want to go further with your SaaS. Kudos to the team.'
}, {
  user: {
    name: 'Estéban Soubiran',
    description: 'Web developer and UnJS member',
    avatar: {
      src: 'https://pbs.twimg.com/profile_images/1801649350319218689/aS_X_iTm_400x400.jpg',
      loading: 'lazy' as const
    }
  },
  quote: 'Nuxt UI Pro is my preferred choice for everything, from a POC to a web platform. It\'s ready to use out-of-the-box and assists me in crafting pixel-perfect UIs. It saves me a significant amount of time while remaining highly customizable. Give it a try, and you won\'t be let down.'
}]
</script>

<template>
  <div class="flex flex-col gap-4 w-full">
    <UMarquee pause-on-hover :overlay="false" :ui="{ root: '[--gap:--spacing(4)]', content: 'w-auto py-1' }">
      <UPageCard
        v-for="(testimonial, index) in testimonials"
        :key="index"
        variant="subtle"
        :description="testimonial.quote"
        :ui="{
          description: 'before:content-[open-quote] after:content-[close-quote] line-clamp-3'
        }"
        class="w-64 shrink-0"
      >
        <template #footer>
          <UUser v-bind="testimonial.user" size="xl" :ui="{ description: 'line-clamp-1' }" />
        </template>
      </UPageCard>
    </UMarquee>
    <UMarquee pause-on-hover reverse :overlay="false" :ui="{ root: '[--gap:--spacing(4)]', content: 'w-auto py-1' }">
      <UPageCard
        v-for="(testimonial, index) in testimonials"
        :key="index"
        variant="subtle"
        :description="testimonial.quote"
        :ui="{
          description: 'before:content-[open-quote] after:content-[close-quote] line-clamp-3'
        }"
        class="w-64 shrink-0"
      >
        <template #footer>
          <UUser v-bind="testimonial.user" size="xl" :ui="{ description: 'line-clamp-1' }" />
        </template>
      </UPageCard>
    </UMarquee>
  </div>
</template>
```

### Screenshots

Use the `Marquee` component to create an infinite scrolling animation for your screenshots.

```vue [MarqueeScreenshots.vue]
<template>
  <div class="relative w-full h-[400px] bg-muted overflow-hidden">
    <UMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -left-[100px] -top-[300px] h-[940px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
      <img
        v-for="i in 4"
        :key="i"
        :src="`/blocks/image${i}.png`"
        width="460"
        height="258"
        :alt="`Nuxt UI Screenshot ${i}`"
        loading="lazy"
        class="aspect-video border border-default rounded-lg bg-white"
      >
    </UMarquee>
    <UMarquee orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -top-[400px] left-[480px] h-[1160px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
      <img
        v-for="i in [5, 6, 7, 8]"
        :key="i"
        :src="`/blocks/image${i}.png`"
        width="460"
        height="258"
        :alt="`Nuxt UI Screenshot ${i}`"
        loading="lazy"
        class="aspect-video border border-default rounded-lg bg-white"
      >
    </UMarquee>
    <UMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: 'hidden md:flex [--duration:40s] absolute w-[460px] -top-[300px] left-[1020px] h-[1060px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
      <img
        v-for="i in [9, 10, 11, 12]"
        :key="i"
        :src="`/blocks/image${i}.png`"
        width="460"
        height="258"
        :alt="`Nuxt UI Screenshot ${i}`"
        loading="lazy"
        class="aspect-video border border-default rounded-lg bg-white"
      >
    </UMarquee>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the Marquee component
 */
interface MarqueeProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Pause the marquee on hover.
   */
  pauseOnHover?: boolean | undefined;
  /**
   * Reverse the direction of the marquee.
   */
  reverse?: boolean | undefined;
  /**
   * The orientation of the marquee.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * The number of times the marquee should repeat.
   * @default "4"
   */
  repeat?: number | undefined;
  /**
   * Display an overlay on the marquee.
   * @default "true"
   */
  overlay?: boolean | undefined;
  ui?: { root?: ClassNameValue; content?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Marquee component
 */
interface MarqueeSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    marquee: {
      slots: {
        root: 'group relative flex items-center overflow-hidden gap-(--gap) [--gap:--spacing(16)] [--duration:20s]',
        content: 'flex items-center shrink-0 justify-around gap-(--gap) min-w-max'
      },
      variants: {
        orientation: {
          horizontal: {
            content: 'w-full'
          },
          vertical: {
            content: 'h-full'
          }
        },
        pauseOnHover: {
          true: {
            content: 'group-hover:[animation-play-state:paused]'
          }
        },
        reverse: {
          true: {
            content: '![animation-direction:reverse]'
          }
        },
        overlay: {
          true: {
            root: 'before:absolute before:pointer-events-none before:content-[""] before:z-2 before:from-default before:to-transparent after:absolute after:pointer-events-none after:content-[""] after:z-2 after:from-default after:to-transparent'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          class: {
            root: 'flex-row',
            content: 'flex-row animate-[marquee_var(--duration)_linear_infinite] rtl:animate-[marquee-rtl_var(--duration)_linear_infinite] backface-hidden'
          }
        },
        {
          orientation: 'horizontal',
          overlay: true,
          class: {
            root: 'before:inset-y-0 before:left-0 before:h-full before:w-1/3 before:bg-gradient-to-r after:inset-y-0 after:right-0 after:h-full after:w-1/3 after:bg-gradient-to-l backface-hidden'
          }
        },
        {
          orientation: 'vertical',
          class: {
            root: 'flex-col',
            content: 'flex-col animate-[marquee-vertical_var(--duration)_linear_infinite] rtl:animate-[marquee-vertical-rtl_var(--duration)_linear_infinite] h-[fit-content] backface-hidden'
          }
        },
        {
          orientation: 'vertical',
          overlay: true,
          class: {
            root: 'before:inset-x-0 before:top-0 before:w-full before:h-1/3 before:bg-gradient-to-b after:inset-x-0 after:bottom-0 after:w-full after:h-1/3 after:bg-gradient-to-t backface-hidden'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Marquee.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/marquee.ts).


# Modal

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Modal.

Then, use the `#content` slot to add the content displayed when the Modal is open.

```vue
<template>
  <UModal>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></UModal>
</template>
```

You can also use the `#header`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, `#body`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} and `#footer`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} slots to customize the Modal's content.

### Title

Use the `title` prop to set the title of the Modal's header.

```vue
<template>
  <UModal title="Modal with title">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Description

Use the `description` prop to set the description of the Modal's header.

```vue
<template>
  <UModal title="Modal with description" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Close

Use the `close` prop to customize or hide the close button (with `false` value) displayed in the Modal's header.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <UModal title="Modal with close button">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

\> \[!TIP]
\> The close button is not displayed if the \`#content\` slot is used as it's a part of the header.

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<template>
  <UModal title="Modal with close button" close-icon="i-lucide-arrow-right">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Transition

Use the `transition` prop to control whether the Modal is animated or not. Defaults to `true`.

```vue
<template>
  <UModal :transition="false" title="Modal without transition">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Overlay

Use the `overlay` prop to control whether the Modal has an overlay or not. Defaults to `true`.

```vue
<template>
  <UModal :overlay="false" title="Modal without overlay">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Modal

Use the `modal` prop to control whether the Modal blocks interaction with outside content. Defaults to `true`.

\> \[!NOTE]
\> When \`modal\` is set to \`false\`, the overlay is automatically disabled and outside content becomes interactive.

```vue
<template>
  <UModal :modal="false" title="Modal interactive">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Dismissible

Use the `dismissible` prop to control whether the Modal is dismissible when clicking outside of it or pressing escape. Defaults to `true`.

\> \[!NOTE]
\> A \`close\:prevent\` event will be emitted when the user tries to close it.

\> \[!TIP]
\> You can combine \`modal: false\` with \`dismissible: false\` to make the Modal's background interactive without closing it.

```vue
<template>
  <UModal :dismissible="false" modal title="Modal non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></UModal>
</template>
```

### Scrollable `4.2+`

Use the `scrollable` prop to make the Modal's content scrollable within the overlay.

\> \[!WARNING]
\> As the overlay is needed for scrolling, \`modal: false\` is not compatible and \`overlay: false\` only removes the background.

```vue
<template>
  <UModal scrollable overlay title="Modal scrollable">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></UModal>
</template>
```

\> \[!CAUTION]
\> There's a \[known issue]\(https\://reka-ui.com/docs/components/dialog#scrollable-overlay) where clicking on the scrollbar may unintentionally close the dialog on some operating systems.

### Fullscreen

Use the `fullscreen` prop to make the Modal fullscreen.

```vue
<template>
  <UModal fullscreen title="Modal fullscreen">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></UModal>
</template>
```

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [ModalOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UModal v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Modal by pressing .

\> \[!TIP]
\> This allows you to move the trigger outside of the Modal or remove it entirely.

### Programmatic usage

You can use the [`useOverlay`](https://ui.nuxt.com/docs/composables/use-overlay) composable to open a Modal programmatically.

\> \[!WARNING]
\> Make sure to wrap your app with the \[\`App\`]\(/docs/components/app) component which uses the \[\`OverlayProvider\`]\(https\://github.com/nuxt/ui/blob/v4/src/runtime/components/OverlayProvider.vue) component.

First, create a modal component that will be opened programmatically:

```vue [ModalExample.vue]
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <UModal :close="{ onClick: () => emit('close', false) }" :title="`This modal was opened programmatically ${count} times`">
    <template #footer>
      <div class="flex gap-2">
        <UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <UButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> We are emitting a \`close\` event when the modal is closed or dismissed here. You can emit any data through the \`close\` event, however, the event must be emitted in order to capture the return value.

Then, use it in your app:

```vue [ModalProgrammaticExample.vue]
<script setup lang="ts">
import { LazyModalExample } from '#components'

const count = ref(0)

const toast = useToast()
const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)

async function open() {
  const instance = modal.open({
    count: count.value
  })

  const shouldIncrement = await instance.result

  if (shouldIncrement) {
    count.value++

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'modal-success'
    })

    // Update the count
    modal.patch({
      count: count.value
    })
    return
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'modal-dismiss'
  })
}
</script>

<template>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
```

\> \[!TIP]
\> You can close the modal within the modal component by emitting \`emit('close')\`.

### Nested modals

You can nest modals within each other.

```vue [ModalNestedExample.vue]
<script setup lang="ts">
const first = ref(false)
const second = ref(false)
</script>

<template>
  <UModal v-model:open="first" title="First modal" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <UModal v-model:open="second" title="Second modal" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </UModal>
    </template>
  </UModal>
</template>
```

### With footer slot

Use the `#footer` slot to add content after the Modal's body.

```vue [ModalFooterSlotExample.vue]
<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <UModal v-model:open="open" title="Modal with footer" description="This is useful when you want a form in a Modal." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </UModal>
</template>
```

### With command palette

You can use a [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) component inside the Modal's content.

```vue [ModalCommandPaletteExample.vue]
<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'modal-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UModal @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UModal>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the Modal opens.

## API

### Props

```ts
/**
 * Props for the Modal component
 */
interface ModalProps {
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The content of the modal.
   */
  content?: (Omit<DialogContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DialogContentImplEmits>>) | undefined;
  /**
   * Render an overlay behind the modal.
   * @default "true"
   */
  overlay?: boolean | undefined;
  /**
   * When `true`, enables scrollable overlay mode where content scrolls within the overlay.
   */
  scrollable?: boolean | undefined;
  /**
   * Animate the modal when opening or closing.
   * @default "true"
   */
  transition?: boolean | undefined;
  /**
   * When `true`, the modal will take up the full screen.
   */
  fullscreen?: boolean | undefined;
  /**
   * Render the modal in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Display a close button to dismiss the modal.
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * When `false`, the modal will not close when clicking outside or pressing escape.
   * @default "true"
   */
  dismissible?: boolean | undefined;
  ui?: { overlay?: ClassNameValue; content?: ClassNameValue; header?: ClassNameValue; wrapper?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; close?: ClassNameValue; } | undefined;
  /**
   * The controlled open state of the dialog. Can be binded as `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The modality of the dialog When set to `true`, <br>
   * interaction with outside elements will be disabled and only dialog content will be visible to screen readers.
   * @default "true"
   */
  modal?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Modal component
 */
interface ModalSlots {
  default(): any;
  content(): any;
  header(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
  body(): any;
  footer(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Modal component
 */
interface ModalEmits {
  after:leave: (payload: []) => void;
  after:enter: (payload: []) => void;
  close:prevent: (payload: []) => void;
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    modal: {
      slots: {
        overlay: 'fixed inset-0',
        content: 'bg-default divide-y divide-default flex flex-col focus:outline-none',
        header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
        wrapper: '',
        body: 'flex-1 p-4 sm:p-6',
        footer: 'flex items-center gap-1.5 p-4 sm:px-6',
        title: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        close: 'absolute top-4 end-4'
      },
      variants: {
        transition: {
          true: {
            overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
            content: 'data-[state=open]:animate-[scale-in_200ms_ease-out] data-[state=closed]:animate-[scale-out_200ms_ease-in]'
          }
        },
        fullscreen: {
          true: {
            content: 'inset-0'
          },
          false: {
            content: 'w-[calc(100vw-2rem)] max-w-lg rounded-lg shadow-lg ring ring-default'
          }
        },
        overlay: {
          true: {
            overlay: 'bg-elevated/75'
          }
        },
        scrollable: {
          true: {
            overlay: 'overflow-y-auto',
            content: 'relative'
          },
          false: {
            content: 'fixed',
            body: 'overflow-y-auto'
          }
        }
      },
      compoundVariants: [
        {
          scrollable: true,
          fullscreen: false,
          class: {
            overlay: 'grid place-items-center p-4 sm:py-8'
          }
        },
        {
          scrollable: false,
          fullscreen: false,
          class: {
            content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] overflow-hidden'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Modal.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/modal.ts).


# NavigationMenu

## Usage

Use the NavigationMenu component to display a list of links horizontally or vertically.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.',
        to: '/docs/components/popover',
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.',
        to: '/docs/components/progress',
      },
    ],
  },
  {
    label: 'GitHub',
    icon: 'i-simple-icons-github',
    badge: '6k',
    to: 'https://github.com/nuxt/ui',
    target: '_blank',
  },
  {
    label: 'Help',
    icon: 'i-lucide-circle-help',
    disabled: true,
  },
])
</script>

<template>
  <UNavigationMenu :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `badge?: string | number | BadgeProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`chip?: boolean | ChipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-chip-in-items)
- [`tooltip?: TooltipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-tooltip-in-items)
- [`popover?: PopoverProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-popover-in-items)
- `trailingIcon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `type?: 'label' | 'trigger' | 'link'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `defaultOpen?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `open?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `value?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `children?: NavigationMenuChildItem[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLeadingChipSize?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childLabel?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.',
        to: '/docs/components/popover',
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.',
        to: '/docs/components/progress',
      },
    ],
  },
  {
    label: 'GitHub',
    icon: 'i-simple-icons-github',
    badge: '6k',
    to: 'https://github.com/nuxt/ui',
    target: '_blank',
  },
  {
    label: 'Help',
    icon: 'i-lucide-circle-help',
    disabled: true,
  },
])
</script>

<template>
  <UNavigationMenu class="w-full justify-center" :items="items" />
</template>
```

\> \[!NOTE]
\> You can also pass an array of arrays to the \`items\` prop to display groups of items.

\> \[!TIP]
\> Each item can take a \`children\` array of objects with the following properties to create submenus:\`label: string\`\`description?: string\`\`icon?: string\`\`onSelect?: (e: Event) => void\`\`class?: any\`

### Orientation

Use the `orientation` prop to change the orientation of the NavigationMenu.

\> \[!NOTE]
\> When orientation is \`vertical\`, an \[Accordion]\(/docs/components/accordion) component is used to display each group. You can control the open state of each item using the \`open\` and \`defaultOpen\` properties and change the behavior using the \[\`collapsible\`]\(/docs/components/accordion#collapsible) and \[\`type\`]\(/docs/components/accordion#multiple) props.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Links',
      type: 'label',
    },
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      children: [
        {
          label: 'Introduction',
          description: 'Fully styled and customizable components for Nuxt.',
          icon: 'i-lucide-house',
        },
        {
          label: 'Installation',
          description: 'Learn how to install and configure Nuxt UI in your application.',
          icon: 'i-lucide-cloud-download',
        },
        {
          label: 'Icons',
          icon: 'i-lucide-smile',
          description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
        },
        {
          label: 'Colors',
          icon: 'i-lucide-swatch-book',
          description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
        },
        {
          label: 'Theme',
          icon: 'i-lucide-cog',
          description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
        },
      ],
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      children: [
        {
          label: 'defineShortcuts',
          icon: 'i-lucide-file-text',
          description: 'Define shortcuts for your application.',
          to: '/docs/composables/define-shortcuts',
        },
        {
          label: 'useOverlay',
          icon: 'i-lucide-file-text',
          description: 'Display a modal/slideover within your application.',
          to: '/docs/composables/use-overlay',
        },
        {
          label: 'useToast',
          icon: 'i-lucide-file-text',
          description: 'Display a toast within your application.',
          to: '/docs/composables/use-toast',
        },
      ],
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      type: 'trigger',
      active: true,
      defaultOpen: true,
      children: [
        {
          label: 'Link',
          icon: 'i-lucide-file-text',
          description: 'Use NuxtLink with superpowers.',
          to: '/docs/components/link',
        },
        {
          label: 'Modal',
          icon: 'i-lucide-file-text',
          description: 'Display a modal within your application.',
          to: '/docs/components/modal',
        },
        {
          label: 'NavigationMenu',
          icon: 'i-lucide-file-text',
          description: 'Display a list of links.',
          to: '/docs/components/navigation-menu',
        },
        {
          label: 'Pagination',
          icon: 'i-lucide-file-text',
          description: 'Display a list of pages.',
          to: '/docs/components/pagination',
        },
        {
          label: 'Popover',
          icon: 'i-lucide-file-text',
          description: 'Display a non-modal dialog that floats around a trigger element.',
          to: '/docs/components/popover',
        },
        {
          label: 'Progress',
          icon: 'i-lucide-file-text',
          description: 'Show a horizontal bar to indicate task progression.',
          to: '/docs/components/progress',
        },
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu orientation="vertical" class="data-[orientation=vertical]:w-48" :items="items" />
</template>
```

\> \[!NOTE]
\> Groups will be spaced when orientation is \`horizontal\` and separated when orientation is \`vertical\`.

### Collapsed

In `vertical` orientation, use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.

\> \[!NOTE]
\> You can use the \[\`tooltip\`]\(#with-tooltip-in-items) and \[\`popover\`]\(#with-popover-in-items) props to display more information on the collapsed items.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Links',
      type: 'label',
    },
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      children: [
        {
          label: 'Introduction',
          description: 'Fully styled and customizable components for Nuxt.',
          icon: 'i-lucide-house',
        },
        {
          label: 'Installation',
          description: 'Learn how to install and configure Nuxt UI in your application.',
          icon: 'i-lucide-cloud-download',
        },
        {
          label: 'Icons',
          icon: 'i-lucide-smile',
          description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
        },
        {
          label: 'Colors',
          icon: 'i-lucide-swatch-book',
          description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
        },
        {
          label: 'Theme',
          icon: 'i-lucide-cog',
          description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
        },
      ],
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      children: [
        {
          label: 'defineShortcuts',
          icon: 'i-lucide-file-text',
          description: 'Define shortcuts for your application.',
          to: '/docs/composables/define-shortcuts',
        },
        {
          label: 'useOverlay',
          icon: 'i-lucide-file-text',
          description: 'Display a modal/slideover within your application.',
          to: '/docs/composables/use-overlay',
        },
        {
          label: 'useToast',
          icon: 'i-lucide-file-text',
          description: 'Display a toast within your application.',
          to: '/docs/composables/use-toast',
        },
      ],
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
      children: [
        {
          label: 'Link',
          icon: 'i-lucide-file-text',
          description: 'Use NuxtLink with superpowers.',
          to: '/docs/components/link',
        },
        {
          label: 'Modal',
          icon: 'i-lucide-file-text',
          description: 'Display a modal within your application.',
          to: '/docs/components/modal',
        },
        {
          label: 'NavigationMenu',
          icon: 'i-lucide-file-text',
          description: 'Display a list of links.',
          to: '/docs/components/navigation-menu',
        },
        {
          label: 'Pagination',
          icon: 'i-lucide-file-text',
          description: 'Display a list of pages.',
          to: '/docs/components/pagination',
        },
        {
          label: 'Popover',
          icon: 'i-lucide-file-text',
          description: 'Display a non-modal dialog that floats around a trigger element.',
          to: '/docs/components/popover',
        },
        {
          label: 'Progress',
          icon: 'i-lucide-file-text',
          description: 'Show a horizontal bar to indicate task progression.',
          to: '/docs/components/progress',
        },
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu collapsed :tooltip="false" :popover="false" orientation="vertical" :items="items" />
</template>
```

### Highlight

Use the `highlight` prop to display a highlighted border for the active item.

Use the `highlight-color` prop to change the color of the border. It defaults to the `color` prop.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      children: [
        {
          label: 'Introduction',
          description: 'Fully styled and customizable components for Nuxt.',
          icon: 'i-lucide-house',
        },
        {
          label: 'Installation',
          description: 'Learn how to install and configure Nuxt UI in your application.',
          icon: 'i-lucide-cloud-download',
        },
        {
          label: 'Icons',
          icon: 'i-lucide-smile',
          description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
        },
        {
          label: 'Colors',
          icon: 'i-lucide-swatch-book',
          description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
        },
        {
          label: 'Theme',
          icon: 'i-lucide-cog',
          description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
        },
      ],
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      children: [
        {
          label: 'defineShortcuts',
          icon: 'i-lucide-file-text',
          description: 'Define shortcuts for your application.',
          to: '/docs/composables/define-shortcuts',
        },
        {
          label: 'useOverlay',
          icon: 'i-lucide-file-text',
          description: 'Display a modal/slideover within your application.',
          to: '/docs/composables/use-overlay',
        },
        {
          label: 'useToast',
          icon: 'i-lucide-file-text',
          description: 'Display a toast within your application.',
          to: '/docs/composables/use-toast',
        },
      ],
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
      defaultOpen: true,
      children: [
        {
          label: 'Link',
          icon: 'i-lucide-file-text',
          description: 'Use NuxtLink with superpowers.',
          to: '/docs/components/link',
        },
        {
          label: 'Modal',
          icon: 'i-lucide-file-text',
          description: 'Display a modal within your application.',
          to: '/docs/components/modal',
        },
        {
          label: 'NavigationMenu',
          icon: 'i-lucide-file-text',
          description: 'Display a list of links.',
          to: '/docs/components/navigation-menu',
        },
        {
          label: 'Pagination',
          icon: 'i-lucide-file-text',
          description: 'Display a list of pages.',
          to: '/docs/components/pagination',
        },
        {
          label: 'Popover',
          icon: 'i-lucide-file-text',
          description: 'Display a non-modal dialog that floats around a trigger element.',
          to: '/docs/components/popover',
        },
        {
          label: 'Progress',
          icon: 'i-lucide-file-text',
          description: 'Show a horizontal bar to indicate task progression.',
          to: '/docs/components/progress',
        },
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu highlight highlight-color="primary" orientation="horizontal" class="data-[orientation=horizontal]:border-b border-default data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-48" :items="items" />
</template>
```

\> \[!NOTE]
\> In this example, the \`border-b\` class is applied to display a border in \`horizontal\` orientation, this is not done by default to let you have a clean slate to work with.

\> \[!CAUTION]
\> In \`vertical\` orientation, the \`highlight\` prop only highlights the border of active children.

### Color

Use the `color` prop to change the color of the NavigationMenu.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      to: '/docs/getting-started',
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      to: '/docs/composables',
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
  ],
])
</script>

<template>
  <UNavigationMenu color="neutral" class="w-full" :items="items" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the NavigationMenu.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      to: '/docs/getting-started',
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      to: '/docs/composables',
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
  ],
])
</script>

<template>
  <UNavigationMenu color="neutral" variant="link" :highlight="false" class="w-full" :items="items" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop changes the \`pill\` variant active item style. Try it out to see the difference.

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon) of each item. Defaults to `i-lucide-chevron-down`. This icon is only displayed when an item has children.

\> \[!TIP]
\> You can also set an icon for a specific item by using the \`trailingIcon\` property in the item object.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.',
        to: '/docs/components/popover',
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.',
        to: '/docs/components/progress',
      },
    ],
  },
])
</script>

<template>
  <UNavigationMenu trailing-icon="i-lucide-arrow-down" class="w-full justify-center" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Arrow

Use the `arrow` prop to display an arrow on the NavigationMenu content when items have children.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.',
        to: '/docs/components/popover',
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.',
        to: '/docs/components/progress',
      },
    ],
  },
])
</script>

<template>
  <UNavigationMenu arrow class="w-full justify-center" :items="items" />
</template>
```

\> \[!NOTE]
\> The arrow is animated to follow the active item.

### Content Orientation

Use the `content-orientation` prop to change the orientation of the content.

\> \[!WARNING]
\> This prop only works when \`orientation\` is \`horizontal\`.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
    ],
  },
])
</script>

<template>
  <UNavigationMenu arrow content-orientation="vertical" class="w-full justify-center" :items="items" />
</template>
```

### Unmount

Use the `unmount-on-hide` prop to control the content unmounting behavior. Defaults to `true`.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[]>([
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house',
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download',
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
      },
    ],
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.',
        to: '/docs/composables/define-shortcuts',
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.',
        to: '/docs/composables/use-overlay',
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.',
        to: '/docs/composables/use-toast',
      },
    ],
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    active: true,
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.',
        to: '/docs/components/link',
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.',
        to: '/docs/components/modal',
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.',
        to: '/docs/components/navigation-menu',
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.',
        to: '/docs/components/pagination',
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.',
        to: '/docs/components/popover',
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.',
        to: '/docs/components/progress',
      },
    ],
  },
])
</script>

<template>
  <UNavigationMenu :unmount-on-hide="false" class="w-full justify-center" :items="items" />
</template>
```

\> \[!NOTE]
\> You can inspect the DOM to see each item's content being rendered.

## Examples

### Control active item

You can control the active item(s) by using the `default-value` prop or the `v-model` directive with the `value` of the item. If no `value` is provided, it defaults to `item-${index}` for top-level items or `item-${level}-${index}` for nested items.

```vue [NavigationMenuModelValueExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    children: [
      {
        label: 'Introduction',
        description: 'Fully styled and customizable components for Nuxt.',
        icon: 'i-lucide-house'
      },
      {
        label: 'Installation',
        description: 'Learn how to install and configure Nuxt UI in your application.',
        icon: 'i-lucide-cloud-download'
      },
      {
        label: 'Icons',
        icon: 'i-lucide-smile',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
      },
      {
        label: 'Colors',
        icon: 'i-lucide-swatch-book',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
      },
      {
        label: 'Theme',
        icon: 'i-lucide-cog',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
      }
    ]
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    children: [
      {
        label: 'defineShortcuts',
        icon: 'i-lucide-file-text',
        description: 'Define shortcuts for your application.'
      },
      {
        label: 'useOverlay',
        icon: 'i-lucide-file-text',
        description: 'Display a modal/slideover within your application.'
      },
      {
        label: 'useToast',
        icon: 'i-lucide-file-text',
        description: 'Display a toast within your application.'
      }
    ]
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    children: [
      {
        label: 'Link',
        icon: 'i-lucide-file-text',
        description: 'Use NuxtLink with superpowers.'
      },
      {
        label: 'Modal',
        icon: 'i-lucide-file-text',
        description: 'Display a modal within your application.'
      },
      {
        label: 'NavigationMenu',
        icon: 'i-lucide-file-text',
        description: 'Display a list of links.'
      },
      {
        label: 'Pagination',
        icon: 'i-lucide-file-text',
        description: 'Display a list of pages.'
      },
      {
        label: 'Popover',
        icon: 'i-lucide-file-text',
        description: 'Display a non-modal dialog that floats around a trigger element.'
      },
      {
        label: 'Progress',
        icon: 'i-lucide-file-text',
        description: 'Show a horizontal bar to indicate task progression.'
      }
    ]
  }
]

const active = ref()

defineShortcuts({
  1: () => {
    active.value = 'item-0'
  },
  2: () => {
    active.value = 'item-1'
  },
  3: () => {
    active.value = 'item-2'
  }
})
</script>

<template>
  <UNavigationMenu v-model="active" :items="items" class="w-full justify-center" />
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to change the key used to match items when a \`v-model\` or \`default-value\` is provided.

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can switch the active item by pressing , , or .

### With tooltip in items

When orientation is `vertical` and the menu is `collapsed`, you can set the `tooltip` prop to `true` to display a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) around items with their label but you can also use the `tooltip` property on each item to override the default tooltip. In `horizontal` orientation, you can use the `tooltip` property on each item to display a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) around items.

\> \[!NOTE]
\> The \`tooltip\` property on an item will always display a tooltip regardless of the global \`tooltip\` prop.

You can pass any property from the [Tooltip](https://ui.nuxt.com/docs/components/tooltip) component globally or on each item.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Links',
      type: 'label',
    },
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      children: [
        {
          label: 'Introduction',
          description: 'Fully styled and customizable components for Nuxt.',
          icon: 'i-lucide-house',
        },
        {
          label: 'Installation',
          description: 'Learn how to install and configure Nuxt UI in your application.',
          icon: 'i-lucide-cloud-download',
        },
        {
          label: 'Icons',
          icon: 'i-lucide-smile',
          description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
        },
        {
          label: 'Colors',
          icon: 'i-lucide-swatch-book',
          description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
        },
        {
          label: 'Theme',
          icon: 'i-lucide-cog',
          description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
        },
      ],
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      children: [
        {
          label: 'defineShortcuts',
          icon: 'i-lucide-file-text',
          description: 'Define shortcuts for your application.',
          to: '/docs/composables/define-shortcuts',
        },
        {
          label: 'useOverlay',
          icon: 'i-lucide-file-text',
          description: 'Display a modal/slideover within your application.',
          to: '/docs/composables/use-overlay',
        },
        {
          label: 'useToast',
          icon: 'i-lucide-file-text',
          description: 'Display a toast within your application.',
          to: '/docs/composables/use-toast',
        },
      ],
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
      children: [
        {
          label: 'Link',
          icon: 'i-lucide-file-text',
          description: 'Use NuxtLink with superpowers.',
          to: '/docs/components/link',
        },
        {
          label: 'Modal',
          icon: 'i-lucide-file-text',
          description: 'Display a modal within your application.',
          to: '/docs/components/modal',
        },
        {
          label: 'NavigationMenu',
          icon: 'i-lucide-file-text',
          description: 'Display a list of links.',
          to: '/docs/components/navigation-menu',
        },
        {
          label: 'Pagination',
          icon: 'i-lucide-file-text',
          description: 'Display a list of pages.',
          to: '/docs/components/pagination',
        },
        {
          label: 'Popover',
          icon: 'i-lucide-file-text',
          description: 'Display a non-modal dialog that floats around a trigger element.',
          to: '/docs/components/popover',
        },
        {
          label: 'Progress',
          icon: 'i-lucide-file-text',
          description: 'Show a horizontal bar to indicate task progression.',
          to: '/docs/components/progress',
        },
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
      tooltip: {
        text: 'Open on GitHub',
        kbds: [
          '6k',
        ],
      },
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu tooltip collapsed orientation="vertical" :items="items" />
</template>
```

### With popover in items

When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](https://ui.nuxt.com/docs/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.

\> \[!NOTE]
\> The \`popover\` property on an item will always display a popover regardless of the global \`popover\` prop.

You can pass any property from the [Popover](https://ui.nuxt.com/docs/components/popover) component globally or on each item.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Links',
      type: 'label',
    },
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      children: [
        {
          label: 'Introduction',
          description: 'Fully styled and customizable components for Nuxt.',
          icon: 'i-lucide-house',
        },
        {
          label: 'Installation',
          description: 'Learn how to install and configure Nuxt UI in your application.',
          icon: 'i-lucide-cloud-download',
        },
        {
          label: 'Icons',
          icon: 'i-lucide-smile',
          description: 'You have nothing to do, @nuxt/icon will handle it automatically.',
        },
        {
          label: 'Colors',
          icon: 'i-lucide-swatch-book',
          description: 'Choose a primary and a neutral color from your Tailwind CSS theme.',
        },
        {
          label: 'Theme',
          icon: 'i-lucide-cog',
          description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.',
        },
      ],
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      popover: {
        mode: 'click',
      },
      children: [
        {
          label: 'defineShortcuts',
          icon: 'i-lucide-file-text',
          description: 'Define shortcuts for your application.',
          to: '/docs/composables/define-shortcuts',
        },
        {
          label: 'useOverlay',
          icon: 'i-lucide-file-text',
          description: 'Display a modal/slideover within your application.',
          to: '/docs/composables/use-overlay',
        },
        {
          label: 'useToast',
          icon: 'i-lucide-file-text',
          description: 'Display a toast within your application.',
          to: '/docs/composables/use-toast',
        },
      ],
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
      children: [
        {
          label: 'Link',
          icon: 'i-lucide-file-text',
          description: 'Use NuxtLink with superpowers.',
          to: '/docs/components/link',
        },
        {
          label: 'Modal',
          icon: 'i-lucide-file-text',
          description: 'Display a modal within your application.',
          to: '/docs/components/modal',
        },
        {
          label: 'NavigationMenu',
          icon: 'i-lucide-file-text',
          description: 'Display a list of links.',
          to: '/docs/components/navigation-menu',
        },
        {
          label: 'Pagination',
          icon: 'i-lucide-file-text',
          description: 'Display a list of pages.',
          to: '/docs/components/pagination',
        },
        {
          label: 'Popover',
          icon: 'i-lucide-file-text',
          description: 'Display a non-modal dialog that floats around a trigger element.',
          to: '/docs/components/popover',
        },
        {
          label: 'Progress',
          icon: 'i-lucide-file-text',
          description: 'Show a horizontal bar to indicate task progression.',
          to: '/docs/components/progress',
        },
      ],
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      badge: '6k',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
      tooltip: {
        text: 'Open on GitHub',
        kbds: [
          '6k',
        ],
      },
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu popover collapsed orientation="vertical" :items="items" />
</template>
```

\> \[!TIP]
\> See: #with-content-slot
\> You can use the \`#content\` slot to customize the content of the popover in the \`vertical\` orientation.

### With chip in items `4.5+`

Use the `chip` property to display a [Chip](https://ui.nuxt.com/docs/components/chip) around the icon of the items, you can pass any of its props.

```vue
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = ref<NavigationMenuItem[][]>([
  [
    {
      label: 'Guide',
      icon: 'i-lucide-book-open',
      chip: {
        color: 'error',
      },
    },
    {
      label: 'Composables',
      icon: 'i-lucide-database',
      chip: {
        color: 'info',
        text: 3,
      },
    },
    {
      label: 'Components',
      icon: 'i-lucide-box',
      to: '/docs/components',
      active: true,
      chip: true,
    },
  ],
  [
    {
      label: 'GitHub',
      icon: 'i-simple-icons-github',
      to: 'https://github.com/nuxt/ui',
      target: '_blank',
    },
    {
      label: 'Help',
      icon: 'i-lucide-circle-help',
      disabled: true,
    },
  ],
])
</script>

<template>
  <UNavigationMenu collapsed orientation="vertical" :items="items" />
</template>
```

### With bottom tab bar

Use the `ui` prop to transform the NavigationMenu into a mobile-style bottom tab bar with icons and small labels, similar to YouTube or Instagram.

```vue [NavigationMenuBottomTabBarExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [
  {
    label: 'Home',
    icon: 'i-lucide-house',
    active: true
  },
  {
    label: 'Samples',
    icon: 'i-lucide-circle-play'
  },
  {
    label: 'Explore',
    icon: 'i-lucide-compass'
  },
  {
    label: 'Library',
    icon: 'i-lucide-bookmark'
  }
]
</script>

<template>
  <UNavigationMenu
    :items="items"
    :ui="{
      root: 'justify-around border-t border-default py-2',
      item: 'py-0',
      link: 'flex-col gap-1 px-3',
      linkLeadingIcon: 'size-5',
      linkLabel: 'text-[10px]/3 font-normal'
    }"
    class="w-full"
  />
</template>
```

### With collapsed labels

Use the `ui` prop to display a label underneath each icon when collapsed.

```vue [NavigationMenuCollapsedLabelExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[] = [
  {
    label: 'Home',
    icon: 'i-lucide-house'
  },
  {
    label: 'Search',
    icon: 'i-lucide-search'
  },
  {
    label: 'Users',
    icon: 'i-lucide-users',
    active: true
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog'
  }
]
</script>

<template>
  <UNavigationMenu
    collapsed
    orientation="vertical"
    :items="items"
    :ui="{
      link: 'flex-col gap-1',
      linkLabel: 'block text-[10px]/3 text-center'
    }"
  />
</template>
```

\> \[!TIP]
\> You can also do this globally through the \`app.config.ts\` using \[\`compoundVariants\`]\(/docs/getting-started/theme/components#compound-variants):
\> \`\`\`ts
\> export default defineAppConfig({
\> ui: {
\> navigationMenu: {
\> compoundVariants: \[{
\> orientation: 'vertical',
\> collapsed: true,
\> class: {
\> link: 'flex-col',
\> linkLabel: 'block text-\[10px]/3 text-center'
\> }
\> }]
\> }
\> }
\> })
\>
\> \`\`\`

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-content`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [NavigationMenuCustomSlotExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, DropdownMenuItem } from '@nuxt/ui'

const items = [
  {
    label: 'Guide',
    icon: 'i-lucide-book-open',
    to: '/docs/getting-started'
  },
  {
    label: 'Composables',
    icon: 'i-lucide-database',
    to: '/docs/composables',
    class: 'hidden'
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    to: '/docs/components',
    class: 'hidden'
  },
  {
    slot: 'more' as const,
    as: 'span',
    class: 'p-0',
    content: {
      align: 'start' as const
    },
    items: [
      {
        label: 'Composables',
        icon: 'i-lucide-database',
        to: '/docs/composables'
      },
      {
        label: 'Components',
        icon: 'i-lucide-box',
        to: '/docs/components'
      }
    ] satisfies DropdownMenuItem[]
  },
  {
    label: 'GitHub',
    icon: 'i-simple-icons-github',
    to: 'https://github.com/nuxt/ui',
    target: '_blank',
    slot: 'github' as const
  }
] satisfies NavigationMenuItem[]
</script>

<template>
  <UNavigationMenu :items="items" class="w-full justify-center">
    <template #more="{ item }">
      <UDropdownMenu :content="item.content" :items="item.items">
        <UButton icon="i-lucide-ellipsis" color="neutral" variant="link" />
      </UDropdownMenu>
    </template>

    <template #github-trailing>
      <UBadge label="6k+" color="neutral" variant="subtle" size="sm" />
    </template>
  </UNavigationMenu>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can also use the \`#item\`, \`#item-leading\`, \`#item-label\`, \`#item-trailing\` and \`#item-content\` slots to customize all items.

### With trailing slot

Use the `#item-trailing` slot or the `slot` property (`#{{ item.slot }}-trailing`) to add a [DropdownMenu](https://ui.nuxt.com/docs/components/dropdown-menu) that appears on hover, similar to Notion or Linear.

```vue [NavigationMenuTrailingSlotExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, DropdownMenuItem } from '@nuxt/ui'

const items: NavigationMenuItem[][] = [
  [
    {
      label: 'Personal',
      type: 'label',
      slot: 'personal-label' as const
    },
    {
      label: 'Design System',
      icon: 'i-lucide-folder',
      active: true
    },
    {
      label: 'Travel',
      icon: 'i-lucide-folder'
    }
  ],
  [
    {
      label: 'Teams',
      type: 'label',
      slot: 'teams-label' as const
    },
    {
      label: 'Engineering',
      icon: 'i-lucide-folder'
    },
    {
      label: 'Marketing',
      icon: 'i-lucide-folder'
    }
  ]
]

const dropdownItems: DropdownMenuItem[][] = [
  [
    { label: 'View Project', icon: 'i-lucide-folder-open' },
    { label: 'Share Project', icon: 'i-lucide-share' }
  ],
  [
    { label: 'Delete Project', icon: 'i-lucide-trash', color: 'error' }
  ]
]
</script>

<template>
  <UNavigationMenu
    orientation="vertical"
    :items="items"
    :ui="{ link: 'overflow-hidden has-data-[state=open]:before:bg-elevated/50' }"
    class="w-48"
  >
    <template #personal-label-trailing>
      <UButton icon="i-lucide-plus" color="neutral" variant="ghost" size="xs" />
    </template>

    <template #teams-label-trailing>
      <UButton icon="i-lucide-plus" color="neutral" variant="ghost" size="xs" />
    </template>

    <template #item-trailing>
      <div class="flex -mr-1.5 -my-0.5 translate-x-full group-hover:translate-x-0 has-data-[state=open]:translate-x-0 transition-transform">
        <UDropdownMenu
          :items="dropdownItems"
          :content="{ align: 'start' }"
          :modal="false"
          size="xs"
        >
          <UButton
            as="div"
            icon="i-lucide-ellipsis"
            color="neutral"
            variant="ghost"
            size="xs"
            class="text-muted hover:text-highlighted hover:bg-accented/50 data-[state=open]:bg-accented/50 mr-1.5"
          />
        </UDropdownMenu>
      </div>
    </template>
  </UNavigationMenu>
</template>
```

### With content slot

Use the `#item-content` slot or the `slot` property (`#{{ item.slot }}-content`) to customize the content of a specific item.

```vue [NavigationMenuContentSlotExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const items = [
  {
    label: 'Docs',
    icon: 'i-lucide-book-open',
    slot: 'docs' as const,
    children: [
      {
        label: 'Icons',
        description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
      },
      {
        label: 'Colors',
        description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
      },
      {
        label: 'Theme',
        description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
      }
    ]
  },
  {
    label: 'Components',
    icon: 'i-lucide-box',
    slot: 'components' as const,
    children: [
      {
        label: 'Link',
        description: 'Use NuxtLink with superpowers.'
      },
      {
        label: 'Modal',
        description: 'Display a modal within your application.'
      },
      {
        label: 'NavigationMenu',
        description: 'Display a list of links.'
      },
      {
        label: 'Pagination',
        description: 'Display a list of pages.'
      },
      {
        label: 'Popover',
        description: 'Display a non-modal dialog that floats around a trigger element.'
      },
      {
        label: 'Progress',
        description: 'Show a horizontal bar to indicate task progression.'
      }
    ]
  },
  {
    label: 'GitHub',
    icon: 'i-simple-icons-github'
  }
] satisfies NavigationMenuItem[]
</script>

<template>
  <UNavigationMenu
    :items="items"
    :ui="{
      viewport: 'sm:w-(--reka-navigation-menu-viewport-width)',
      content: 'sm:w-auto',
      childList: 'sm:w-96',
      childLinkDescription: 'text-balance line-clamp-2'
    }"
    class="w-full justify-center"
  >
    <template #docs-content="{ item }">
      <ul class="grid gap-2 p-4 lg:w-[500px] lg:grid-cols-[minmax(0,.75fr)_minmax(0,1fr)]">
        <li class="row-span-3">
          <Placeholder class="size-full min-h-48" />
        </li>

        <li v-for="child in item.children" :key="child.label">
          <ULink class="text-sm text-left rounded-md p-3 transition-colors hover:bg-elevated/50">
            <p class="font-medium text-highlighted">
              {{ child.label }}
            </p>
            <p class="text-muted line-clamp-2">
              {{ child.description }}
            </p>
          </ULink>
        </li>
      </ul>
    </template>
  </UNavigationMenu>
</template>
```

\> \[!NOTE]
\> In this example, we add the \`sm\:w-(--reka-navigation-menu-viewport-width)\` class on the \`viewport\` to have a dynamic width. This requires to set a width on the content's first child.

## API

### Props

```ts
/**
 * Props for the NavigationMenu component
 */
interface NavigationMenuProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Determines whether a "single" or "multiple" items can be selected at a time.
   * 
   * Only works when `orientation` is `vertical`.
   * @default "\"multiple\" as never"
   */
  type?: K | undefined;
  /**
   * The controlled value of the active item(s).
   * - In horizontal orientation: always `string`
   * - In vertical orientation with `type="single"`: `string`
   * - In vertical orientation with `type="multiple"`: `string[]`
   * 
   * Use this when you need to control the state of the items. Can be binded with `v-model`
   */
  modelValue?: NavigationMenuModelValue<K, O> | undefined;
  /**
   * The default active value of the item(s).
   * - In horizontal orientation: always `string`
   * - In vertical orientation with `type="single"`: `string`
   * - In vertical orientation with `type="multiple"`: `string[]`
   * 
   * Use when you do not need to control the state of the item(s).
   */
  defaultValue?: NavigationMenuModelValue<K, O> | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when the item is an external link.
   * Set to `false` to hide the external icon.
   * @default "true"
   */
  externalIcon?: any;
  items?: T | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "pill" | "link" | undefined;
  /**
   * The orientation of the menu.
   * @default "\"horizontal\" as never"
   */
  orientation?: O | undefined;
  /**
   * Collapse the navigation menu to only show icons.
   * Only works when `orientation` is `vertical`.
   */
  collapsed?: boolean | undefined;
  /**
   * Display a tooltip on the items with the label of the item.
   * Only works when `orientation` is `vertical` and `collapsed` is `true`.
   * `{ delayDuration: 0, content: { side: 'right' } }`{lang="ts-type"}
   */
  tooltip?: boolean | TooltipProps | undefined;
  /**
   * Display a popover on the items when the menu is collapsed with the children list.
   * `{ mode: 'hover', content: { side: 'right', align: 'start', alignOffset: 2 } }`{lang="ts-type"}
   */
  popover?: boolean | PopoverProps<PopoverMode> | undefined;
  /**
   * Display a line next to the active item.
   */
  highlight?: boolean | undefined;
  highlightColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The content of the menu.
   */
  content?: (Omit<NavigationMenuContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DismissableLayerEmits>>) | undefined;
  /**
   * The orientation of the content.
   * Only works when `orientation` is `horizontal`.
   * @default "\"horizontal\""
   */
  contentOrientation?: "horizontal" | "vertical" | undefined;
  /**
   * Display an arrow alongside the menu.
   */
  arrow?: boolean | undefined;
  /**
   * The key used to get the value from the item.
   * @default "\"value\""
   */
  valueKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  ui?: { root?: ClassNameValue; list?: ClassNameValue; label?: ClassNameValue; item?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLeadingAvatar?: ClassNameValue; linkLeadingAvatarSize?: ClassNameValue; linkLeadingChipSize?: ClassNameValue; linkTrailing?: ClassNameValue; linkTrailingBadge?: ClassNameValue; linkTrailingBadgeSize?: ClassNameValue; linkTrailingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkLabelExternalIcon?: ClassNameValue; childList?: ClassNameValue; childLabel?: ClassNameValue; childItem?: ClassNameValue; childLink?: ClassNameValue; childLinkWrapper?: ClassNameValue; childLinkIcon?: ClassNameValue; childLinkLabel?: ClassNameValue; childLinkLabelExternalIcon?: ClassNameValue; childLinkDescription?: ClassNameValue; separator?: ClassNameValue; viewportWrapper?: ClassNameValue; viewport?: ClassNameValue; content?: ClassNameValue; indicator?: ClassNameValue; arrow?: ClassNameValue; } | undefined;
  /**
   * The duration from when the pointer enters the trigger until the tooltip gets opened.
   * @default "0"
   */
  delayDuration?: number | undefined;
  /**
   * If `true`, menu cannot be open by click on trigger
   */
  disableClickTrigger?: boolean | undefined;
  /**
   * If `true`, menu cannot be open by hover on trigger
   */
  disableHoverTrigger?: boolean | undefined;
  /**
   * How much time a user has to enter another trigger without incurring a delay again.
   */
  skipDelayDuration?: number | undefined;
  /**
   * If `true`, menu will not close during pointer leave event
   */
  disablePointerLeaveClose?: boolean | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "true"
   */
  unmountOnHide?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with the accordion and all its items
   */
  disabled?: boolean | undefined;
  /**
   * When type is "single", allows closing content when clicking trigger for an open item.
   * When type is "multiple", this prop has no effect.
   * @default "true"
   */
  collapsible?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the NavigationMenu component
 */
interface NavigationMenuSlots {
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-trailing(): any;
  item-content(): any;
  list-leading(): any;
  list-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the NavigationMenu component
 */
interface NavigationMenuEmits {
  update:modelValue: (payload: [value: NavigationMenuModelValue<K, O> | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    navigationMenu: {
      slots: {
        root: 'relative flex gap-1.5 [&>div]:min-w-0',
        list: 'isolate min-w-0',
        label: 'w-full flex items-center gap-1.5 font-semibold text-xs/5 text-highlighted px-2.5 py-1.5',
        item: 'min-w-0',
        link: 'group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
        linkLeadingIcon: 'shrink-0 size-5',
        linkLeadingAvatar: 'shrink-0',
        linkLeadingAvatarSize: '2xs',
        linkLeadingChipSize: 'sm',
        linkTrailing: 'group ms-auto inline-flex gap-1.5 items-center',
        linkTrailingBadge: 'shrink-0',
        linkTrailingBadgeSize: 'sm',
        linkTrailingIcon: 'size-5 transform shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        linkLabel: 'truncate',
        linkLabelExternalIcon: 'inline-block size-3 align-top text-dimmed',
        childList: 'isolate',
        childLabel: 'text-xs text-highlighted',
        childItem: '',
        childLink: 'group relative size-full flex items-start text-start text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
        childLinkWrapper: 'min-w-0',
        childLinkIcon: 'size-5 shrink-0',
        childLinkLabel: 'truncate',
        childLinkLabelExternalIcon: 'inline-block size-3 align-top text-dimmed',
        childLinkDescription: 'text-muted',
        separator: 'px-2 h-px bg-border',
        viewportWrapper: 'absolute top-full left-0 flex w-full',
        viewport: 'relative overflow-hidden bg-default shadow-lg rounded-md ring ring-default h-(--reka-navigation-menu-viewport-height) w-full transition-[width,height,left,right] duration-200 origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] z-1',
        content: '',
        indicator: 'absolute left-0 data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] data-[state=hidden]:opacity-0 bottom-0 z-2 w-(--reka-navigation-menu-indicator-size) translate-x-(--reka-navigation-menu-indicator-position) flex h-2.5 items-end justify-center overflow-hidden transition-[translate,width] duration-200',
        arrow: 'relative top-[50%] size-2.5 rotate-45 border border-default bg-default z-1 rounded-xs'
      },
      variants: {
        color: {
          primary: {
            link: 'focus-visible:before:ring-primary',
            childLink: 'focus-visible:before:ring-primary'
          },
          secondary: {
            link: 'focus-visible:before:ring-secondary',
            childLink: 'focus-visible:before:ring-secondary'
          },
          success: {
            link: 'focus-visible:before:ring-success',
            childLink: 'focus-visible:before:ring-success'
          },
          info: {
            link: 'focus-visible:before:ring-info',
            childLink: 'focus-visible:before:ring-info'
          },
          warning: {
            link: 'focus-visible:before:ring-warning',
            childLink: 'focus-visible:before:ring-warning'
          },
          error: {
            link: 'focus-visible:before:ring-error',
            childLink: 'focus-visible:before:ring-error'
          },
          neutral: {
            link: 'focus-visible:before:ring-inverted',
            childLink: 'focus-visible:before:ring-inverted'
          }
        },
        highlightColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          pill: '',
          link: ''
        },
        orientation: {
          horizontal: {
            root: 'items-center justify-between',
            list: 'flex items-center',
            item: 'py-2',
            link: 'px-2.5 py-1.5 before:inset-x-px before:inset-y-0',
            childList: 'grid p-2',
            childLink: 'px-3 py-2 gap-2 before:inset-x-px before:inset-y-0',
            childLinkLabel: 'font-medium',
            content: 'absolute top-0 left-0 w-full max-h-[70vh] overflow-y-auto'
          },
          vertical: {
            root: 'flex-col',
            link: 'flex-row px-2.5 py-1.5 before:inset-y-px before:inset-x-0',
            childLabel: 'px-1.5 py-0.5',
            childLink: 'p-1.5 gap-1.5 before:inset-y-px before:inset-x-0'
          }
        },
        contentOrientation: {
          horizontal: {
            viewportWrapper: 'justify-center',
            content: 'data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]'
          },
          vertical: {
            viewport: 'sm:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left) rtl:left-auto rtl:right-[calc(100%-var(--reka-navigation-menu-viewport-left)-var(--reka-navigation-menu-viewport-width))]'
          }
        },
        active: {
          true: {
            childLink: 'before:bg-elevated text-highlighted',
            childLinkIcon: 'text-default'
          },
          false: {
            link: 'text-muted',
            linkLeadingIcon: 'text-dimmed',
            childLink: [
              'hover:before:bg-elevated/50 text-default hover:text-highlighted',
              'transition-colors before:transition-colors'
            ],
            childLinkIcon: [
              'text-dimmed group-hover:text-default',
              'transition-colors'
            ]
          }
        },
        disabled: {
          true: {
            link: 'cursor-not-allowed opacity-75'
          }
        },
        highlight: {
          true: ''
        },
        level: {
          true: ''
        },
        collapsed: {
          true: ''
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          contentOrientation: 'horizontal',
          class: {
            childList: 'grid-cols-2 gap-2'
          }
        },
        {
          orientation: 'horizontal',
          contentOrientation: 'vertical',
          class: {
            childList: 'gap-1',
            content: 'w-60'
          }
        },
        {
          orientation: 'vertical',
          collapsed: false,
          class: {
            childList: 'ms-5 border-s border-default',
            childItem: 'ps-1.5 -ms-px',
            content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden'
          }
        },
        {
          orientation: 'vertical',
          collapsed: true,
          class: {
            link: 'px-1.5',
            linkLabel: 'hidden',
            linkTrailing: 'hidden',
            content: 'shadow-sm rounded-sm min-h-6 p-1'
          }
        },
        {
          orientation: 'horizontal',
          highlight: true,
          class: {
            link: [
              'after:absolute after:-bottom-2 after:inset-x-2.5 after:block after:h-px after:rounded-full',
              'after:transition-colors'
            ]
          }
        },
        {
          orientation: 'vertical',
          highlight: true,
          level: true,
          class: {
            link: [
              'after:absolute after:-start-1.5 after:inset-y-0.5 after:block after:w-px after:rounded-full',
              'after:transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'pill',
          class: {
            link: [
              'hover:text-highlighted hover:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ],
            linkLeadingIcon: [
              'group-hover:text-default',
              'transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'pill',
          orientation: 'horizontal',
          class: {
            link: 'data-[state=open]:text-highlighted',
            linkLeadingIcon: 'group-data-[state=open]:text-default'
          }
        },
        {
          disabled: false,
          variant: 'pill',
          highlight: true,
          orientation: 'horizontal',
          class: {
            link: 'data-[state=open]:before:bg-elevated/50'
          }
        },
        {
          disabled: false,
          variant: 'pill',
          highlight: false,
          active: false,
          orientation: 'horizontal',
          class: {
            link: 'data-[state=open]:before:bg-elevated/50'
          }
        },
        {
          color: 'primary',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-primary',
            linkLeadingIcon: 'text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-secondary',
            linkLeadingIcon: 'text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-success',
            linkLeadingIcon: 'text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-info',
            linkLeadingIcon: 'text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-warning',
            linkLeadingIcon: 'text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-error',
            linkLeadingIcon: 'text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          active: true,
          class: {
            link: 'text-highlighted',
            linkLeadingIcon: 'text-highlighted group-data-[state=open]:text-highlighted'
          }
        },
        {
          variant: 'pill',
          active: true,
          highlight: false,
          class: {
            link: 'before:bg-elevated'
          }
        },
        {
          variant: 'pill',
          active: true,
          highlight: true,
          disabled: false,
          class: {
            link: [
              'hover:before:bg-elevated/50',
              'before:transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'link',
          class: {
            link: [
              'hover:text-highlighted',
              'transition-colors'
            ],
            linkLeadingIcon: [
              'group-hover:text-default',
              'transition-colors'
            ]
          }
        },
        {
          disabled: false,
          active: false,
          variant: 'link',
          orientation: 'horizontal',
          class: {
            link: 'data-[state=open]:text-highlighted',
            linkLeadingIcon: 'group-data-[state=open]:text-default'
          }
        },
        {
          color: 'primary',
          variant: 'link',
          active: true,
          class: {
            link: 'text-primary',
            linkLeadingIcon: 'text-primary group-data-[state=open]:text-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'link',
          active: true,
          class: {
            link: 'text-secondary',
            linkLeadingIcon: 'text-secondary group-data-[state=open]:text-secondary'
          }
        },
        {
          color: 'success',
          variant: 'link',
          active: true,
          class: {
            link: 'text-success',
            linkLeadingIcon: 'text-success group-data-[state=open]:text-success'
          }
        },
        {
          color: 'info',
          variant: 'link',
          active: true,
          class: {
            link: 'text-info',
            linkLeadingIcon: 'text-info group-data-[state=open]:text-info'
          }
        },
        {
          color: 'warning',
          variant: 'link',
          active: true,
          class: {
            link: 'text-warning',
            linkLeadingIcon: 'text-warning group-data-[state=open]:text-warning'
          }
        },
        {
          color: 'error',
          variant: 'link',
          active: true,
          class: {
            link: 'text-error',
            linkLeadingIcon: 'text-error group-data-[state=open]:text-error'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          active: true,
          class: {
            link: 'text-highlighted',
            linkLeadingIcon: 'text-highlighted group-data-[state=open]:text-highlighted'
          }
        },
        {
          highlightColor: 'primary',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-primary'
          }
        },
        {
          highlightColor: 'secondary',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-secondary'
          }
        },
        {
          highlightColor: 'success',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-success'
          }
        },
        {
          highlightColor: 'info',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-info'
          }
        },
        {
          highlightColor: 'warning',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-warning'
          }
        },
        {
          highlightColor: 'error',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-error'
          }
        },
        {
          highlightColor: 'neutral',
          highlight: true,
          level: true,
          active: true,
          class: {
            link: 'after:bg-inverted'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        highlightColor: 'primary',
        variant: 'pill'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/NavigationMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/navigation-menu.ts).


# Page

## Usage

The Page component helps you create layouts with optional left and right columns. It's perfect for building documentation sites and other content-focused pages.

```vue {2,6}
<template>
  <UPage>
    <template #left />

    <template #right />
  </UPage>
</template>
```

\> \[!TIP]
\> The page will display as a centered single column layout if no slots are specified.

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a layout

Use the Page component in a layout with the `left` slot to display a navigation:

```vue [layouts/docs.vue] {9-13}
<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>
```

\> \[!NOTE]
\> In this example, we use the \`ContentNavigation\` component to display the navigation injected in \`app.vue\`.

### Within a page

Use the Page component in a page with the `right` slot to display a table of contents:

```vue [pages/[...slug\\].vue] {29-31}
<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('content', 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>
```

\> \[!NOTE]
\> In this example, we use the \`ContentToc\` component to display the table of contents.

## API

### Props

```ts
/**
 * Props for the Page component
 */
interface PageProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { root?: ClassNameValue; left?: ClassNameValue; center?: ClassNameValue; right?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Page component
 */
interface PageSlots {
  left(): any;
  default(): any;
  right(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    page: {
      slots: {
        root: 'flex flex-col lg:grid lg:grid-cols-10 lg:gap-10',
        left: 'lg:col-span-2',
        center: 'lg:col-span-8',
        right: 'lg:col-span-2 order-first lg:order-last'
      },
      variants: {
        left: {
          true: ''
        },
        right: {
          true: ''
        }
      },
      compoundVariants: [
        {
          left: true,
          right: true,
          class: {
            center: 'lg:col-span-6'
          }
        },
        {
          left: false,
          right: false,
          class: {
            center: 'lg:col-span-10'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Page.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page.ts).


# PageAnchors

## Usage

Use the PageAnchors component to display a list of links.

```vue
<script setup lang="ts">
import type { PageAnchor } from '@nuxt/ui'
</script>

<template>
  <UPageAnchors />
</template>
```

### Links

Use the `links` prop as an array of objects with the following properties:

- `label: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkLeading?: ClassNameValue, linkLeadingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { PageAnchor } from '@nuxt/ui'
</script>

<template>
  <UPageAnchors />
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a layout

Use the PageAnchors component inside the [PageAside](https://ui.nuxt.com/docs/components/page-aside) component to display a list of links above the navigation.

```vue [layouts/docs.vue] {35}
<script setup lang="ts">
import type { PageAnchor } from '@nuxt/ui'
import type { ContentNavigationItem } from '@nuxt/content'

const navigation = inject<ContentNavigationItem[]>('navigation')

const links: PageAnchor[] = [{
  label: 'Documentation',
  icon: 'i-lucide-book-open',
  to: '/docs/getting-started'
}, {
  label: 'Components',
  icon: 'i-lucide-box',
  to: '/docs/components'
}, {
  label: 'Figma Kit',
  icon: 'i-simple-icons-figma',
  to: 'https://go.nuxt.com/figma-ui',
  target: '_blank'
}, {
  label: 'Releases',
  icon: 'i-lucide-rocket',
  to: 'https://github.com/nuxt/ui/releases',
  target: '_blank'
}]
</script>

<template>
  <UPage>
    <template #left>
      <UPageAside>
        <UPageAnchors :links="links" />

        <USeparator type="dashed" />

        <UContentNavigation :navigation="navigation" />
      </UPageAside>
    </template>

    <slot />
  </UPage>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageAnchors component
 */
interface PageAnchorsProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  links?: T[] | undefined;
  ui?: { root?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; link?: ClassNameValue; linkLeading?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkLabelExternalIcon?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageAnchors component
 */
interface PageAnchorsSlots {
  link(): any;
  link-leading(): any;
  link-label(): any;
  link-trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageAnchors: {
      slots: {
        root: '',
        list: '',
        item: 'relative',
        link: 'group text-sm flex items-center gap-1.5 py-1 focus-visible:outline-primary',
        linkLeading: 'rounded-md p-1 inline-flex ring-inset ring',
        linkLeadingIcon: 'size-4 shrink-0',
        linkLabel: 'truncate',
        linkLabelExternalIcon: 'size-3 absolute top-0 text-dimmed'
      },
      variants: {
        active: {
          true: {
            link: 'text-primary font-semibold',
            linkLeading: 'bg-primary ring-primary text-inverted'
          },
          false: {
            link: [
              'text-muted hover:text-default font-medium',
              'transition-colors'
            ],
            linkLeading: [
              'bg-elevated/50 ring-accented text-dimmed group-hover:bg-primary group-hover:ring-primary group-hover:text-inverted',
              'transition'
            ]
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageAnchors.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-anchors.ts).


# PageAside

## Usage

The PageAside component is a sticky `<aside>` element that is only displayed starting from the [`lg` breakpoint](https://tailwindcss.com/docs/breakpoints){rel="&#x22;nofollow&#x22;"}.

\> \[!TIP]
\> See: /docs/getting-started/theme/css-variables#header
\> The PageAside component uses the \`--ui-header-height\` CSS variable to position itself correctly below the \`Header\`.

Use it inside the `left` or `right` slot of the [Page](https://ui.nuxt.com/docs/components/page) component:

```vue {4}
<template>
  <UPage>
    <template #left>
      <UPageAside />
    </template>
  </UPage>
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a layout

Use the PageAside component in a layout to display the navigation:

```vue [layouts/docs.vue] {9-13}
<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>
```

\> \[!NOTE]
\> In this example, we use the \`ContentNavigation\` component to display the navigation injected in \`app.vue\`.

## API

### Props

```ts
/**
 * Props for the PageAside component
 */
interface PageAsideProps {
  /**
   * The element or component this component should render as.
   * @default "\"aside\""
   */
  as?: any;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; top?: ClassNameValue; topHeader?: ClassNameValue; topBody?: ClassNameValue; topFooter?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageAside component
 */
interface PageAsideSlots {
  top(): any;
  default(): any;
  bottom(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageAside: {
      slots: {
        root: 'hidden overflow-y-auto lg:block lg:max-h-[calc(100vh-var(--ui-header-height))] lg:sticky lg:top-(--ui-header-height) py-8 lg:ps-4 lg:-ms-4 lg:pe-6.5',
        container: 'relative',
        top: 'sticky -top-8 -mt-8 pointer-events-none z-[1]',
        topHeader: 'h-8 bg-default -mx-4 px-4',
        topBody: 'bg-default relative pointer-events-auto flex flex-col -mx-4 px-4',
        topFooter: 'h-8 bg-gradient-to-b from-default -mx-4 px-4'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageAside.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-aside.ts).


# PageBody

## Usage

The PageBody component wraps your main content and adds some padding for consistent spacing.

Use it inside the default slot of the [Page](https://ui.nuxt.com/docs/components/page) component, after the [PageHeader](https://ui.nuxt.com/docs/components/page-header) component:

```vue {5}
<template>
  <UPage>
    <UPageHeader />

    <UPageBody />
  </UPage>
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the PageBody component in a page to display the content of the page:

```vue [pages/[...slug\\].vue] {21-27}
<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('content', 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>
```

\> \[!NOTE]
\> In this example, we use the \[\`ContentRenderer\`]\(https\://content.nuxt.com/docs/components/content-renderer) component from \`@nuxt/content\` to render the content of the page.

## API

### Props

```ts
/**
 * Props for the PageBody component
 */
interface PageBodyProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageBody component
 */
interface PageBodySlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageBody: {
      base: 'mt-8 pb-24 space-y-12'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageBody.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-body.ts).


# PageCard

## Usage

The PageCard component provides a flexible way to display content in a card with an illustration in the default slot.

```vue
<template>
  <u-page-card description=Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements. icon=i-simple-icons-tailwindcss title=Tailwind CSS>
  <img alt=Tailwind CSS src=/tailwindcss-v4.svg /></u-page-card>
</template>
```

\> \[!TIP]
\> Use the \[PageGrid]\(/docs/components/page-grid), \[PageColumns]\(/docs/components/page-columns) or \[PageList]\(/docs/components/page-list) components to display multiple PageCard.

### Title

Use the `title` prop to set the title of the card.

```vue
<template>
  <UPageCard title="Tailwind CSS" />
</template>
```

### Description

Use the `description` prop to set the description of the card.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." />
</template>
```

### Icon

Use the `icon` prop to set the icon of the card.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" />
</template>
```

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" to="https://tailwindcss.com/docs/v4-beta" target="_blank" />
</template>
```

### Variant

Use the `variant` prop to change the style of the card.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" to="https://tailwindcss.com/docs/v4-beta" target="_blank" variant="soft" />
</template>
```

\> \[!TIP]
\> You can apply the \`light\` or \`dark\` class to the \`links\` slot when using the \`solid\` variant to reverse the colors.

### Orientation

Use the `orientation` prop to change the orientation with the default slot. Defaults to `vertical`.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" orientation="horizontal">
    <img src="/tailwindcss-v4.svg" alt="Tailwind CSS" class="w-full" />
  </UPageCard>
</template>
```

### Reverse

Use the `reverse` prop to reverse the orientation of the default slot.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" orientation="horizontal" reverse>
    <img src="/tailwindcss-v4.svg" alt="Tailwind CSS" class="w-full" />
  </UPageCard>
</template>
```

### Highlight

Use the `highlight` and `highlight-color` props to display a highlighted border around the card.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" orientation="horizontal" highlight highlight-color="primary">
    <img src="/tailwindcss-v4.svg" alt="Tailwind CSS" class="w-full" />
  </UPageCard>
</template>
```

### Spotlight

Use the `spotlight` and `spotlight-color` props to display a spotlight effect that follows your mouse cursor and highlights borders on hover.

\> \[!NOTE]
\> The spotlight effect will take over hover effects when using a \`to\` prop. It's best to use it with the \`outline\` variant.

```vue
<template>
  <UPageCard title="Tailwind CSS" description="Nuxt UI integrates with latest Tailwind CSS, bringing significant improvements." icon="i-simple-icons-tailwindcss" orientation="horizontal" spotlight spotlight-color="primary">
    <img src="/tailwindcss-v4.svg" alt="Tailwind CSS" class="w-full" />
  </UPageCard>
</template>
```

\> \[!TIP]
\> You can also customize the color and size by using the \`--spotlight-color\` and \`--spotlight-size\` CSS variables:
\> \`\`\`vue
\> \<template>
\> \<UPageCard spotlight class="\[--spotlight-color\:var(--ui-error)] \[--spotlight-size:200px]" />
\> \</template>
\>
\> \`\`\`

## Examples

### As a testimonial

Use the [User](https://ui.nuxt.com/docs/components/user) component in the `header` or `footer` slot to make the card look like a testimonial.

```vue [PageCardTestimonialExample.vue]
<script setup lang="ts">
const testimonial = ref({
  user: {
    name: 'Evan You',
    description: 'Author of Vue.js and Vite',
    avatar: {
      src: 'https://avatars.githubusercontent.com/u/499550?v=4',
      alt: 'Evan You',
      loading: 'lazy' as const
    }
  },
  quote: '“Nuxt on Cloudflare infra with minimal effort - this is huge!”'
})
</script>

<template>
  <UPageCard :description="testimonial.quote" class="w-60">
    <template #footer>
      <UUser v-bind="testimonial.user" />
    </template>
  </UPageCard>
</template>
```

\> \[!TIP]
\> See: /docs/components/page-columns
\> You can use the \`PageColumns\` component to display multiple PageCard in a multi-column layout.

## API

### Props

```ts
/**
 * Props for the PageCard component
 */
interface PageCardProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed above the title.
   */
  icon?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The orientation of the page card.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Reverse the order of the default slot.
   */
  reverse?: boolean | undefined;
  /**
   * Display a line around the page card.
   */
  highlight?: boolean | undefined;
  highlightColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * Display a spotlight effect that follows your mouse cursor and highlights borders on hover.
   */
  spotlight?: boolean | undefined;
  spotlightColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "naked" | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | undefined;
  ui?: { root?: ClassNameValue; spotlight?: ClassNameValue; container?: ClassNameValue; wrapper?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageCard component
 */
interface PageCardSlots {
  header(): any;
  body(): any;
  leading(): any;
  title(): any;
  description(): any;
  footer(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageCard: {
      slots: {
        root: 'relative flex rounded-lg',
        spotlight: 'absolute inset-0 rounded-[inherit] pointer-events-none bg-default/90',
        container: 'relative flex flex-col flex-1 lg:grid gap-x-8 gap-y-4 p-4 sm:p-6',
        wrapper: 'flex flex-col flex-1 items-start',
        header: 'mb-4',
        body: 'flex-1',
        footer: 'pt-4 mt-auto',
        leading: 'inline-flex items-center mb-2.5',
        leadingIcon: 'size-5 shrink-0 text-primary',
        title: 'text-base text-pretty font-semibold text-highlighted',
        description: 'text-[15px] text-pretty'
      },
      variants: {
        orientation: {
          horizontal: {
            container: 'lg:grid-cols-2 lg:items-center'
          },
          vertical: {
            container: ''
          }
        },
        reverse: {
          true: {
            wrapper: 'order-last'
          }
        },
        variant: {
          solid: {
            root: 'bg-inverted text-inverted',
            title: 'text-inverted',
            description: 'text-dimmed'
          },
          outline: {
            root: 'bg-default ring ring-default',
            description: 'text-muted'
          },
          soft: {
            root: 'bg-elevated/50',
            description: 'text-toned'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default',
            description: 'text-toned'
          },
          ghost: {
            description: 'text-muted'
          },
          naked: {
            container: 'p-0 sm:p-0',
            description: 'text-muted'
          }
        },
        to: {
          true: {
            root: [
              'has-focus-visible:ring-2 has-focus-visible:ring-primary',
              'transition'
            ]
          }
        },
        title: {
          true: {
            description: 'mt-1'
          }
        },
        highlight: {
          true: {
            root: 'ring-2'
          }
        },
        highlightColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        spotlight: {
          true: {
            root: '[--spotlight-size:400px] before:absolute before:-inset-px before:pointer-events-none before:rounded-[inherit] before:bg-[radial-gradient(var(--spotlight-size)_var(--spotlight-size)_at_calc(var(--spotlight-x,0px))_calc(var(--spotlight-y,0px)),var(--spotlight-color),transparent_70%)]'
          }
        },
        spotlightColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        }
      },
      compoundVariants: [
        {
          variant: 'solid',
          to: true,
          class: {
            root: 'hover:bg-inverted/90'
          }
        },
        {
          variant: 'outline',
          to: true,
          class: {
            root: 'hover:bg-elevated/50'
          }
        },
        {
          variant: 'soft',
          to: true,
          class: {
            root: 'hover:bg-elevated'
          }
        },
        {
          variant: 'subtle',
          to: true,
          class: {
            root: 'hover:bg-elevated'
          }
        },
        {
          variant: 'subtle',
          to: true,
          highlight: false,
          class: {
            root: 'hover:ring-accented'
          }
        },
        {
          variant: 'ghost',
          to: true,
          class: {
            root: 'hover:bg-elevated/50'
          }
        },
        {
          highlightColor: 'primary',
          highlight: true,
          class: {
            root: 'ring-primary'
          }
        },
        {
          highlightColor: 'secondary',
          highlight: true,
          class: {
            root: 'ring-secondary'
          }
        },
        {
          highlightColor: 'success',
          highlight: true,
          class: {
            root: 'ring-success'
          }
        },
        {
          highlightColor: 'info',
          highlight: true,
          class: {
            root: 'ring-info'
          }
        },
        {
          highlightColor: 'warning',
          highlight: true,
          class: {
            root: 'ring-warning'
          }
        },
        {
          highlightColor: 'error',
          highlight: true,
          class: {
            root: 'ring-error'
          }
        },
        {
          highlightColor: 'neutral',
          highlight: true,
          class: {
            root: 'ring-inverted'
          }
        },
        {
          spotlightColor: 'primary',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-primary)]'
          }
        },
        {
          spotlightColor: 'secondary',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-secondary)]'
          }
        },
        {
          spotlightColor: 'success',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-success)]'
          }
        },
        {
          spotlightColor: 'info',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-info)]'
          }
        },
        {
          spotlightColor: 'warning',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-warning)]'
          }
        },
        {
          spotlightColor: 'error',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-error)]'
          }
        },
        {
          spotlightColor: 'neutral',
          spotlight: true,
          class: {
            root: '[--spotlight-color:var(--ui-bg-inverted)]'
          }
        }
      ],
      defaultVariants: {
        variant: 'outline',
        highlightColor: 'primary',
        spotlightColor: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageCard.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-card.ts).


# PageColumns

## Usage

The PageColumns component displays content in a responsive multi-column layout. It works well with [PageCard](https://ui.nuxt.com/docs/components/page-card) components or any other elements, adapting from single column on mobile to multiple columns on larger screens.

```vue [PageColumnsExample.vue]
<script setup lang="ts">
const testimonials = ref([
  {
    user: {
      name: 'Evan You',
      description: 'Author of Vue.js and Vite',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/499550?v=4',
        alt: 'Evan You',
        loading: 'lazy' as const
      }
    },
    quote: 'Nuxt on Cloudflare infra with minimal effort - this is huge!'
  },
  {
    user: {
      name: 'Igor Minar',
      description: 'Software Engineer at Cloudflare',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/216296?v=4',
        alt: 'Igor Minar',
        loading: 'lazy' as const
      }
    },
    quote: 'I love the polish and the batteries-included approach. NuxtHub takes web framework and hosting integration to a new level!'
  },
  {
    user: {
      name: 'Charlie Hield',
      description: 'Senior Creative Technologist',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/527849?v=4',
        alt: 'Charlie Hield',
        loading: 'lazy' as const
      }
    },
    quote: 'NuxtHub is hands down the easiest way to get a project from zero to production on the Cloudflare stack!'
  },
  {
    user: {
      name: 'Israel Ortuño',
      description: 'Co-founder of VueJobs',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/1769417?v=4',
        alt: 'Israel Ortuño',
        loading: 'lazy' as const
      }
    },
    quote: 'I can\'t find an excuse to not go full-stack with Nuxt from now on. Ship fast the Nuxt way, zero config. Just plug & deploy.'
  },
  {
    user: {
      name: 'Fayaz Ahmed',
      description: 'Indie Hacker',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/15716057?v=4',
        alt: 'Fayaz Ahmed',
        loading: 'lazy' as const
      }
    },
    quote: 'Took me less than 90 seconds to deploy an app with DB, KV, File storage and Caching, all on the edge with just a single command.'
  },
  {
    user: {
      name: 'Tommy J. Vedvik',
      description: 'UX Developer',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/48070?v=4',
        alt: 'Tommy J. Vedvik',
        loading: 'lazy' as const
      }
    },
    quote: 'Nuxt is becoming the best framework for bootstrappers imo. NuxtHub is a layer on top of Cloudflare services for cheap & fast full-stack edge hosting.'
  },
  {
    user: {
      name: 'Dario Piotrowicz',
      description: 'Web Developer at Cloudflare',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/61631103?v=4',
        alt: 'Dario Piotrowicz',
        loading: 'lazy' as const
      }
    },
    quote: 'I love how NuxtHub combines, amplifies and simplifies the Cloudflare tooling with the wide and mature Nuxt ecosystem. I cannot wait to see how it will evolve and expand in the future!'
  },
  {
    user: {
      name: 'Markus Oberlehner',
      description: 'Web Developer',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/6883314?v=4',
        alt: 'Markus Oberlehner',
        loading: 'lazy' as const
      }
    },
    quote: 'Just deployed my first site to Cloudflare using NuxtHub. Very sleek experience!'
  },
  {
    user: {
      name: 'Anthony Fu',
      description: 'Core team Vue.js, Vite & Nuxt',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/11247099?v=4',
        alt: 'Anthony Fu',
        loading: 'lazy' as const
      }
    },
    quote: 'It\'s amazing to be able to run a single command and get existing Nuxt project deployed on edge within minutes! It felt like unlocking the missing infrastructure and UI for Cloudflare, enhancing the developer experience in such an extraordinary way.'
  },
  {
    user: {
      name: 'Jonathan Beckman',
      description: 'Founder of GuaranTee Time',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/90707158?v=4',
        alt: 'Jonathan Beckman',
        loading: 'lazy' as const
      }
    },
    quote: 'NuxtHub and Cloudflare are my go to for full stack apps. The DX is joyous and far superior to any other platform I\'ve used. My team is able to iterate quickly, and build beautiful, performant apps with ease.'
  },
  {
    user: {
      name: 'Eckhardt Dreyer',
      description: 'Lead Developer at YG',
      avatar: {
        src: 'https://avatars.githubusercontent.com/u/37825447?v=4',
        alt: 'Eckhardt Dreyer',
        loading: 'lazy' as const
      }
    },
    quote: 'At YG, our team recently grew and that meant more seats on all the tools we use. Migrating our hosting workflow to NuxtHub not only took just a few minutes but saved us money from our previous provider. NuxtHub provides an excellent management layer on top of our infrastructure and we\'re super happy about the move!'
  }
])
</script>

<template>
  <UPageColumns>
    <UPageCard
      variant="solid"
      to="https://cloudflare.com"
      icon="i-logos-cloudflare-icon"
      title="Cloudflare's Workers LaunchPad"
      description="NuxtHub is part of the Cloudflare's Workers Launchpad Cohort to make sure you get a first-class experience on top of Cloudflare's network."
      :ui="{ leadingIcon: 'size-10' }"
    />

    <UPageCard
      v-for="(testimonial, index) in testimonials"
      :key="index"
      variant="subtle"
      :description="testimonial.quote"
      :ui="{ description: 'before:content-[open-quote] after:content-[close-quote]' }"
    >
      <template #footer>
        <UUser v-bind="testimonial.user" size="xl" />
      </template>
    </UPageCard>
  </UPageColumns>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageColumns component
 */
interface PageColumnsProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageColumns component
 */
interface PageColumnsSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageColumns: {
      base: 'relative column-1 md:columns-2 lg:columns-3 gap-8 space-y-8 *:break-inside-avoid-column *:will-change-transform'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageColumns.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-columns.ts).


# PageCTA

## Usage

The PageCTA component provides a flexible way to display a call to action in your pages with an illustration in the default slot.

```vue
<template>
  <u-page-c-t-a :links=[{"label":"Get started","color":"neutral"},{"label":"Learn more","color":"neutral","variant":"subtle","trailingIcon":"i-lucide-arrow-right"}] description=Preview the latest Tailwind CSS and get started with Nuxt UI. orientation=horizontal title=Trusted and supported by our amazing community>
  <img alt=Illustration src=https://picsum.photos/640/616 /></u-page-c-t-a>
</template>
```

Use it inside a [PageSection](https://ui.nuxt.com/docs/components/page-section) component or directly in your page:

```vue {4,8-10}
<template>
  <UPageHero />

  <UPageCTA class="rounded-none" />

  <UPageSection />

  <UPageSection :ui="{ container: 'px-0' }">
    <UPageCTA class="rounded-none sm:rounded-xl" />
  </UPageSection>

  <UPageSection />
</template>
```

\> \[!TIP]
\> Use \`px-0\` and \`rounded-none\` classes to make the CTA fill the edge of the page on mobile.

### Title

Use the `title` prop to set the title of the CTA.

```vue
<template>
  <UPageCTA title="Trusted and supported by our amazing community" />
</template>
```

### Description

Use the `description` prop to set the description of the CTA.

```vue
<template>
  <UPageCTA title="Trusted and supported by our amazing community" description="We've built a strong, lasting partnership. Their trust is our driving force, propelling us towards shared success." />
</template>
```

### Links

Use the `links` prop to display a list of [Button](https://ui.nuxt.com/docs/components/button) under the description.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageCTA title="Trusted and supported by our amazing community" description="We've built a strong, lasting partnership. Their trust is our driving force, propelling us towards shared success." />
</template>
```

### Variant

Use the `variant` prop to change the style of the CTA.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageCTA title="Trusted and supported by our amazing community" description="We've built a strong, lasting partnership. Their trust is our driving force, propelling us towards shared success." variant="soft" />
</template>
```

\> \[!TIP]
\> You can apply the \`light\` or \`dark\` class to the \`links\` slot when using the \`solid\` variant to reverse the colors.

### Orientation

Use the `orientation` prop to change the orientation with the default slot. Defaults to `vertical`.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageCTA title="Trusted and supported by our amazing community" description="We've built a strong, lasting partnership. Their trust is our driving force, propelling us towards shared success." orientation="horizontal">
    <img src="https://picsum.photos/640/728" width="320" height="364" alt="Illustration" class="w-full rounded-lg" loading="lazy" />
  </UPageCTA>
</template>
```

### Reverse

Use the `reverse` prop to reverse the orientation of the default slot.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageCTA title="Trusted and supported by our amazing community" description="We've built a strong, lasting partnership. Their trust is our driving force, propelling us towards shared success." orientation="horizontal" reverse>
    <img src="https://picsum.photos/640/728" width="320" height="364" alt="Illustration" class="w-full rounded-lg" loading="lazy" />
  </UPageCTA>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageCTA component
 */
interface PageCTAProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The orientation of the page cta.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Reverse the order of the default slot.
   * @default "false"
   */
  reverse?: boolean | undefined;
  variant?: "outline" | "solid" | "soft" | "subtle" | "naked" | undefined;
  /**
   * Display a list of Button under the description.
   * `{ size: 'lg' }`{lang="ts-type"}
   */
  links?: ButtonProps[] | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; wrapper?: ClassNameValue; header?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; links?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageCTA component
 */
interface PageCTASlots {
  top(): any;
  header(): any;
  title(): any;
  description(): any;
  body(): any;
  footer(): any;
  links(): any;
  default(): any;
  bottom(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageCTA: {
      slots: {
        root: 'relative isolate rounded-xl overflow-hidden',
        container: 'flex flex-col lg:grid px-6 py-12 sm:px-12 sm:py-24 lg:px-16 lg:py-24 gap-8 sm:gap-16',
        wrapper: '',
        header: '',
        title: 'text-3xl sm:text-4xl text-pretty tracking-tight font-bold text-highlighted',
        description: 'text-base sm:text-lg text-muted',
        body: 'mt-8',
        footer: 'mt-8',
        links: 'flex flex-wrap gap-x-6 gap-y-3'
      },
      variants: {
        orientation: {
          horizontal: {
            container: 'lg:grid-cols-2 lg:items-center',
            description: 'text-pretty'
          },
          vertical: {
            container: '',
            title: 'text-center',
            description: 'text-center text-balance',
            links: 'justify-center'
          }
        },
        reverse: {
          true: {
            wrapper: 'order-last'
          }
        },
        variant: {
          solid: {
            root: 'bg-inverted text-inverted',
            title: 'text-inverted',
            description: 'text-dimmed'
          },
          outline: {
            root: 'bg-default ring ring-default',
            description: 'text-muted'
          },
          soft: {
            root: 'bg-elevated/50',
            description: 'text-toned'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default',
            description: 'text-toned'
          },
          naked: {
            description: 'text-muted'
          }
        },
        title: {
          true: {
            description: 'mt-6'
          }
        }
      },
      defaultVariants: {
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageCTA.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-cta.ts).


# PageFeature

## Usage

The PageFeature component is used by the [PageSection](https://ui.nuxt.com/docs/components/page-section) component to display [features](https://ui.nuxt.com/docs/components/page-section#features).

### Title

Use the `title` prop to set the title of the feature.

```vue
<template>
  <UPageFeature title="Theme" />
</template>
```

### Description

Use the `description` prop to set the description of the feature.

```vue
<template>
  <UPageFeature title="Theme" description="Customize Nuxt UI with your own colors, fonts, and more." />
</template>
```

### Icon

Use the `icon` prop to set the icon of the feature.

```vue
<template>
  <UPageFeature title="Theme" description="Customize Nuxt UI with your own colors, fonts, and more." icon="i-lucide-swatch-book" />
</template>
```

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UPageFeature title="Theme" description="Customize Nuxt UI with your own colors, fonts, and more." icon="i-lucide-swatch-book" to="/docs/getting-started/theme/design-system" target="_blank" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the feature. Defaults to `horizontal`.

```vue
<template>
  <UPageFeature orientation="vertical" title="Theme" description="Customize Nuxt UI with your own colors, fonts, and more." icon="i-lucide-swatch-book" />
</template>
```

## API

### Props

```ts
/**
 * Props for the PageFeature component
 */
interface PageFeatureProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon displayed next to the title when `orientation` is `horizontal` and above the title when `orientation` is `vertical`.
   */
  icon?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The orientation of the page feature.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | undefined;
  ui?: { root?: ClassNameValue; wrapper?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageFeature component
 */
interface PageFeatureSlots {
  leading(): any;
  title(): any;
  description(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageFeature: {
      slots: {
        root: 'relative rounded-sm',
        wrapper: '',
        leading: 'inline-flex items-center justify-center',
        leadingIcon: 'size-5 shrink-0 text-primary',
        title: 'text-base text-pretty font-semibold text-highlighted',
        description: 'text-[15px] text-pretty text-muted'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'flex items-start gap-2.5',
            leading: 'p-0.5'
          },
          vertical: {
            leading: 'mb-2.5'
          }
        },
        to: {
          true: {
            root: [
              'has-focus-visible:ring-2 has-focus-visible:ring-primary',
              'transition'
            ]
          }
        },
        title: {
          true: {
            description: 'mt-1'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageFeature.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-feature.ts).


# PageGrid

## Usage

The PageGrid component provides a responsive grid layout for displaying [PageCard](https://ui.nuxt.com/docs/components/page-card) components or any other elements, automatically adjusting from 1 to 3 columns based on screen size.

```vue [PageGridExample.vue]
<script setup lang="ts">
const cards = ref([
  {
    title: 'Icons',
    description: 'Nuxt UI integrates with Nuxt Icon to access over 200,000+ icons from Iconify.',
    icon: 'i-lucide-smile',
    to: '/docs/getting-started/integrations/icons'
  },
  {
    title: 'Fonts',
    description: 'Nuxt UI integrates with Nuxt Fonts to provide plug-and-play font optimization.',
    icon: 'i-lucide-a-large-small',
    to: '/docs/getting-started/integrations/fonts'
  },
  {
    title: 'Color Mode',
    description: 'Nuxt UI integrates with Nuxt Color Mode to switch between light and dark.',
    icon: 'i-lucide-sun-moon',
    to: '/docs/getting-started/integrations/color-mode'
  }
])
</script>

<template>
  <UPageGrid>
    <UPageCard
      v-for="(card, index) in cards"
      :key="index"
      v-bind="card"
    />
  </UPageGrid>
</template>
```

You can also use it to display a list of cards in a bento style layout by using `col-span-*` and `row-span-*` utility classes.

```vue [PageGridBentoExample.vue]
<script setup lang="ts">
const cards = ref([
  {
    title: 'Theme',
    description: 'Learn how to customize Nuxt UI components using Tailwind CSS.',
    icon: 'i-lucide-swatch-book',
    to: '/docs/getting-started/theme/design-system',
    class: 'lg:col-span-2',
    image: {
      path: 'https://ui2.nuxt.com/illustrations/color-palette',
      width: 363,
      height: 152
    },
    orientation: 'horizontal' as const
  },
  {
    title: 'Fonts',
    description: 'Nuxt UI integrates with Nuxt Fonts to provide plug-and-play font optimization.',
    icon: 'i-lucide-a-large-small',
    to: '/docs/getting-started/integrations/fonts',
    variant: 'soft' as const
  },
  {
    title: 'Color Mode',
    description: 'Nuxt UI integrates with Nuxt Color Mode to switch between light and dark.',
    icon: 'i-lucide-sun-moon',
    to: '/docs/getting-started/integrations/color-mode',
    variant: 'soft' as const
  },
  {
    title: 'Icons',
    description: 'Nuxt UI integrates with Nuxt Icon to access over 200,000+ icons from Iconify.',
    icon: 'i-lucide-smile',
    to: '/docs/getting-started/integrations/icons',
    image: {
      path: 'https://ui2.nuxt.com/illustrations/icon-library',
      width: 362,
      height: 184
    },
    class: 'lg:col-span-2',
    orientation: 'horizontal' as const,
    reverse: true
  }
])
</script>

<template>
  <UPageGrid>
    <UPageCard
      v-for="(card, index) in cards"
      :key="index"
      v-bind="card"
    >
      <UColorModeImage
        v-if="card.image"
        :light="`${card.image.path}-light.svg`"
        :dark="`${card.image.path}-dark.svg`"
        :width="card.image.width"
        :height="card.image.height"
        :alt="card.title"
        loading="lazy"
        class="w-full"
      />
    </UPageCard>
  </UPageGrid>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageGrid component
 */
interface PageGridProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageGrid component
 */
interface PageGridSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageGrid: {
      base: 'relative grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageGrid.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-grid.ts).


# PageHeader

## Usage

The PageHeader component displays a header for your page.

Use it inside the default slot of the [Page](https://ui.nuxt.com/docs/components/page) component, before the [PageBody](https://ui.nuxt.com/docs/components/page-body) component:

```vue {3}
<template>
  <UPage>
    <UPageHeader />

    <UPageBody />
  </UPage>
</template>
```

### Title

Use the `title` prop to display a title in the header.

```vue
<template>
  <UPageHeader title="PageHeader" />
</template>
```

### Description

Use the `description` prop to display a description in the header.

```vue
<template>
  <UPageHeader title="PageHeader" description="A responsive page header with title, description and actions." />
</template>
```

### Headline

Use the `headline` prop to display a headline in the header.

```vue
<template>
  <UPageHeader title="PageHeader" description="A responsive page header with title, description and actions." headline="Components" />
</template>
```

### Links

Use the `links` prop to display a list of [Button](https://ui.nuxt.com/docs/components/button) in the header.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageHeader title="PageHeader" description="A responsive page header with title, description and actions." headline="Components" />
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the PageHeader component in a page to display the header of the page:

```vue [pages/[...slug\\].vue] {19-24}
<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('content', route.path)
})
</script>

<template>
  <UPage>
    <UPageHeader
      :title="page.title"
      :description="page.description"
      :headline="page.headline"
      :links="page.links"
    />

    <UPageBody>
      <ContentRenderer :value="page" />

      <USeparator />

      <UContentSurround :surround="surround" />
    </UPageBody>

    <template #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageHeader component
 */
interface PageHeaderProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  headline?: string | undefined;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Display a list of Button next to the title.
   * `{ color: 'neutral', variant: 'outline' }`{lang="ts-type"}
   */
  links?: ButtonProps[] | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; wrapper?: ClassNameValue; headline?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; links?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageHeader component
 */
interface PageHeaderSlots {
  headline(): any;
  title(): any;
  description(): any;
  links(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageHeader: {
      slots: {
        root: 'relative border-b border-default py-8',
        container: '',
        wrapper: 'flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4',
        headline: 'mb-2.5 text-sm font-semibold text-primary flex items-center gap-1.5',
        title: 'text-3xl sm:text-4xl text-pretty font-bold text-highlighted',
        description: 'text-lg text-pretty text-muted',
        links: 'flex flex-wrap items-center gap-1.5'
      },
      variants: {
        title: {
          true: {
            description: 'mt-4'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageHeader.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-header.ts).


# PageHero

## Usage

The PageHero component wraps your content in a [Container](https://ui.nuxt.com/docs/components/container) while maintaining full-width flexibility making it easy to add background colors, images or patterns. It provides a flexible way to display content with an illustration in the default slot.

```vue
<template>
  <u-page-hero description=A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications. title=Ultimate Vue UI library>
  <u-page-card variant=subtle>
  <template unwrap=p v-slot:default=>
  <p>
  <img alt=App screenshot src=/blocks/image4.png /></p></template></u-page-card></u-page-hero>
</template>
```

### Title

Use the `title` prop to set the title of the hero.

```vue
<template>
  <UPageHero title="Ultimate Vue UI library" />
</template>
```

### Description

Use the `description` prop to set the description of the hero.

```vue
<template>
  <UPageHero title="Ultimate Vue UI library" description="A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications." />
</template>
```

### Headline

Use the `headline` prop to set the headline of the hero.

```vue
<template>
  <UPageHero title="Ultimate Vue UI library" description="A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications." headline="New release" />
</template>
```

### Links

Use the `links` prop to display a list of [Button](https://ui.nuxt.com/docs/components/button) under the description.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageHero title="Ultimate Vue UI library" description="A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications." />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation with the default slot. Defaults to `vertical`.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageHero title="Ultimate Vue UI library" description="A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications." headline="New release" orientation="horizontal">
    <img src="/blocks/image4.png" alt="App screenshot" class="rounded-lg shadow-2xl ring ring-default" />
  </UPageHero>
</template>
```

### Reverse

Use the `reverse` prop to reverse the orientation of the default slot.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageHero title="Ultimate Vue UI library" description="A Nuxt/Vue-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications." headline="New release" orientation="horizontal" reverse>
    <img src="/blocks/image4.png" alt="App screenshot" class="rounded-lg shadow-2xl ring ring-default" />
  </UPageHero>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageHero component
 */
interface PageHeroProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  headline?: string | undefined;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Display a list of Button under the description.
   * `{ size: 'xl' }`{lang="ts-type"}
   */
  links?: ButtonProps[] | undefined;
  /**
   * The orientation of the page hero.
   * @default "\"vertical\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * Reverse the order of the default slot.
   */
  reverse?: boolean | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; wrapper?: ClassNameValue; header?: ClassNameValue; headline?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; links?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageHero component
 */
interface PageHeroSlots {
  top(): any;
  header(): any;
  headline(): any;
  title(): any;
  description(): any;
  body(): any;
  footer(): any;
  links(): any;
  default(): any;
  bottom(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageHero: {
      slots: {
        root: 'relative isolate',
        container: 'flex flex-col lg:grid py-24 sm:py-32 lg:py-40 gap-16 sm:gap-y-24',
        wrapper: '',
        header: '',
        headline: 'mb-4',
        title: 'text-5xl sm:text-7xl text-pretty tracking-tight font-bold text-highlighted',
        description: 'text-lg sm:text-xl/8 text-muted',
        body: 'mt-10',
        footer: 'mt-10',
        links: 'flex flex-wrap gap-x-6 gap-y-3'
      },
      variants: {
        orientation: {
          horizontal: {
            container: 'lg:grid-cols-2 lg:items-center',
            description: 'text-pretty'
          },
          vertical: {
            container: '',
            headline: 'justify-center',
            wrapper: 'text-center',
            description: 'text-balance',
            links: 'justify-center'
          }
        },
        reverse: {
          true: {
            wrapper: 'order-last'
          }
        },
        headline: {
          true: {
            headline: 'font-semibold text-primary flex items-center gap-1.5'
          }
        },
        title: {
          true: {
            description: 'mt-6'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageHero.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-hero.ts).


# PageLinks

## Usage

Use the PageLinks component to display a list of links.

```vue
<script setup lang="ts">
import type { PageLink } from '@nuxt/ui'
</script>

<template>
  <UPageLinks />
</template>
```

### Links

Use the `links` prop as an array of objects with the following properties:

- `label: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkLeadingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { PageLink } from '@nuxt/ui'
</script>

<template>
  <UPageLinks />
</template>
```

### Title

Use the `title` prop to display a title above the links.

```vue
<script setup lang="ts">
import type { PageLink } from '@nuxt/ui'
</script>

<template>
  <UPageLinks title="Community" />
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the PageLinks component in the `bottom` slot of the ContentToc component to display a list of links below the table of contents.

```vue [pages/[...slug\\].vue] {48-52}
<script setup lang="ts">
import type { PageLink } from '@nuxt/ui'

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('content', route.path)
})

const links = computed<PageLink[]>(() => [{
  icon: 'i-lucide-file-pen',
  label: 'Edit this page',
  to: `https://github.com/nuxt/ui/edit/v4/docs/content/${page?.value?.stem}.md`,
  target: '_blank'
}, {
  icon: 'i-lucide-star',
  label: 'Star on GitHub',
  to: 'https://github.com/nuxt/ui',
  target: '_blank'
}, {
  label: 'Releases',
  icon: 'i-lucide-rocket',
  to: 'https://github.com/nuxt/ui/releases'
}])
</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 #bottom>
          <USeparator type="dashed" />

          <UPageLinks title="Community" :links="links" />
        </template>
      </UContentToc>
    </template>
  </UPage>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageLinks component
 */
interface PageLinksProps {
  /**
   * The element or component this component should render as.
   * @default "\"nav\""
   */
  as?: any;
  title?: string | undefined;
  links?: T[] | undefined;
  ui?: { root?: ClassNameValue; title?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkLabelExternalIcon?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageLinks component
 */
interface PageLinksSlots {
  title(): any;
  link(): any;
  link-leading(): any;
  link-label(): any;
  link-trailing(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageLinks: {
      slots: {
        root: 'flex flex-col gap-3',
        title: 'text-sm font-semibold flex items-center gap-1.5',
        list: 'flex flex-col gap-2',
        item: 'relative',
        link: 'group text-sm flex items-center gap-1.5 focus-visible:outline-primary',
        linkLeadingIcon: 'size-5 shrink-0',
        linkLabel: 'truncate',
        linkLabelExternalIcon: 'size-3 absolute top-0 text-dimmed'
      },
      variants: {
        active: {
          true: {
            link: 'text-primary font-medium'
          },
          false: {
            link: [
              'text-muted hover:text-default',
              'transition-colors'
            ]
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageLinks.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-links.ts).


# PageList

## Usage

The PageList component provides a flexible way to display content in a vertical list layout. It's perfect for creating stacked lists of [PageCard](https://ui.nuxt.com/docs/components/page-card) components or any other elements, with optional dividers between items.

```vue [PageListExample.vue]
<script setup lang="ts">
const users = ref([
  {
    name: 'Benjamin Canac',
    description: 'benjamincanac',
    to: 'https://github.com/benjamincanac',
    target: '_blank',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Romain Hamel',
    description: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png',
      alt: 'romhml',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Sébastien Chopin',
    description: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png',
      alt: 'atinux',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Hugo Richard',
    description: 'HugoRCD',
    to: 'https://github.com/HugoRCD',
    target: '_blank',
    avatar: {
      src: 'https://github.com/HugoRCD.png',
      alt: 'HugoRCD',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Sandro Circi',
    description: 'sandros94',
    to: 'https://github.com/sandros94',
    target: '_blank',
    avatar: {
      src: 'https://github.com/sandros94.png',
      alt: 'sandros94',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Daniel Roe',
    description: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'danielroe',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Jakub Michálek',
    description: 'J-Michalek',
    to: 'https://github.com/J-Michalek',
    target: '_blank',
    avatar: {
      src: 'https://github.com/J-Michalek.png',
      alt: 'J-Michalek',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Eugen Istoc',
    description: 'genu',
    to: 'https://github.com/genu',
    target: '_blank',
    avatar: {
      src: 'https://github.com/genu.png',
      alt: 'genu',
      loading: 'lazy' as const
    }
  }
])
</script>

<template>
  <UPageList>
    <UPageCard
      v-for="(user, index) in users"
      :key="index"
      variant="ghost"
      :to="user.to"
      :target="user.target"
    >
      <template #body>
        <UUser :name="user.name" :description="user.description" :avatar="user.avatar" size="xl" class="relative" />
      </template>
    </UPageCard>
  </UPageList>
</template>
```

### Divide

Use the `divide` prop to add a divider between each child element.

```vue [PageListDivideExample.vue]
<script setup lang="ts">
const users = ref([
  {
    name: 'Benjamin Canac',
    description: 'benjamincanac',
    to: 'https://github.com/benjamincanac',
    target: '_blank',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Romain Hamel',
    description: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png',
      alt: 'romhml',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Sébastien Chopin',
    description: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png',
      alt: 'atinux',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Hugo Richard',
    description: 'HugoRCD',
    to: 'https://github.com/HugoRCD',
    target: '_blank',
    avatar: {
      src: 'https://github.com/HugoRCD.png',
      alt: 'HugoRCD',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Sandro Circi',
    description: 'sandros94',
    to: 'https://github.com/sandros94',
    target: '_blank',
    avatar: {
      src: 'https://github.com/sandros94.png',
      alt: 'sandros94',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Daniel Roe',
    description: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png',
      alt: 'danielroe',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Jakub Michálek',
    description: 'J-Michalek',
    to: 'https://github.com/J-Michalek',
    target: '_blank',
    avatar: {
      src: 'https://github.com/J-Michalek.png',
      alt: 'J-Michalek',
      loading: 'lazy' as const
    }
  },
  {
    name: 'Eugen Istoc',
    description: 'genu',
    to: 'https://github.com/genu',
    target: '_blank',
    avatar: {
      src: 'https://github.com/genu.png',
      alt: 'genu',
      loading: 'lazy' as const
    }
  }
])
</script>

<template>
  <UPageList divide>
    <UPageCard
      v-for="(user, index) in users"
      :key="index"
      variant="ghost"
      :to="user.to"
      :target="user.target"
    >
      <template #body>
        <UUser :name="user.name" :description="user.description" :avatar="user.avatar" size="xl" />
      </template>
    </UPageCard>
  </UPageList>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageList component
 */
interface PageListProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * @default "false"
   */
  divide?: boolean | undefined;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageList component
 */
interface PageListSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageList: {
      base: 'relative flex flex-col',
      variants: {
        divide: {
          true: '*:not-last:after:absolute *:not-last:after:inset-x-1 *:not-last:after:bottom-0 *:not-last:after:bg-border *:not-last:after:h-px'
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageList.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-list.ts).


# PageLogos

## Usage

The PageLogos component provides a flexible way to display a list of logos or images in your pages.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'i-simple-icons-github',
  'i-simple-icons-discord',
  'i-simple-icons-x',
  'i-simple-icons-instagram',
  'i-simple-icons-linkedin',
  'i-simple-icons-facebook',
])
</script>

<template>
  <UPageLogos :items="items" />
</template>
```

### Title

Use the `title` prop to set the title above the logos.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'i-simple-icons-github',
  'i-simple-icons-discord',
  'i-simple-icons-x',
  'i-simple-icons-instagram',
  'i-simple-icons-linkedin',
  'i-simple-icons-facebook',
])
</script>

<template>
  <UPageLogos title="Trusted by the best front-end teams" :items="items" />
</template>
```

### Items

You can display logos in two ways:

1. Using the `items` prop to provide a list of logos. Each item can be either:

- An icon name (e.g., `i-simple-icons-github`)
- An object containing `src` and `alt` properties for images, which will be utilized in a `UAvatar` component

2. Using the default slot to have complete control over the content

\`\`\`vue
\<script setup lang="ts">
const items = \[
'i-simple-icons-github',
'i-simple-icons-discord',
'i-simple-icons-x',
'i-simple-icons-instagram',
'i-simple-icons-linkedin',
'i-simple-icons-facebook'
]
\</script>
\<template>
\<UPageLogos title="Trusted by the best front-end teams" \:items="items" />
\</template>
\`\`\`
\`\`\`vue
\<template>
\<UPageLogos title="Trusted by the best front-end teams">
\<UIcon name="i-simple-icons-github" class="size-10 shrink-0" />
\<UIcon name="i-simple-icons-discord" class="size-10 shrink-0" />
\<UIcon name="i-simple-icons-x" class="size-10 shrink-0" />
\<UIcon name="i-simple-icons-instagram" class="size-10 shrink-0" />
\<UIcon name="i-simple-icons-linkedin" class="size-10 shrink-0" />
\<UIcon name="i-simple-icons-facebook" class="size-10 shrink-0" />
\</UPageLogos>
\</template>
\`\`\`

### Marquee

Use the `marquee` prop to enable a marquee effect for the logos.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'i-simple-icons-github',
  'i-simple-icons-discord',
  'i-simple-icons-x',
  'i-simple-icons-instagram',
  'i-simple-icons-linkedin',
  'i-simple-icons-facebook',
])
</script>

<template>
  <UPageLogos title="Trusted by the best front-end teams" marquee :items="items" />
</template>
```

\> \[!NOTE]
\> See: /docs/components/marquee
\> When you use \`marquee\` mode, you can customize its behavior by passing props. For more info, check out the \`Marquee\` component.

## API

### Props

```ts
/**
 * Props for the PageLogos component
 */
interface PageLogosProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: string | undefined;
  items?: PageLogosItem[] | undefined;
  /**
   * @default "false"
   */
  marquee?: boolean | MarqueeProps | undefined;
  ui?: { root?: ClassNameValue; title?: ClassNameValue; logos?: ClassNameValue; logo?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageLogos component
 */
interface PageLogosSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageLogos: {
      slots: {
        root: 'relative overflow-hidden',
        title: 'text-lg text-center font-semibold text-highlighted',
        logos: 'mt-10',
        logo: 'size-10 shrink-0'
      },
      variants: {
        marquee: {
          false: {
            logos: 'flex items-center shrink-0 justify-around gap-(--gap) [--gap:--spacing(16)]'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageLogos.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-logos.ts).


# PageSection

## Usage

The PageSection component wraps your content in a [Container](https://ui.nuxt.com/docs/components/container) while maintaining full-width flexibility making it easy to add background colors, images or patterns. It provides a flexible way to display content with an illustration in the default slot.

```vue
<template>
  <u-page-section :features=[{"title":"Icons","description":"Nuxt UI integrates with Nuxt Icon to access over 200,000+ icons from Iconify.","icon":"i-lucide-smile","to":"/docs/getting-started/integrations/icons"},{"title":"Fonts","description":"Nuxt UI integrates with Nuxt Fonts to provide plug-and-play font optimization.","icon":"i-lucide-a-large-small","to":"/docs/getting-started/integrations/fonts"},{"title":"Color Mode","description":"Nuxt UI integrates with Nuxt Color Mode to switch between light and dark.","icon":"i-lucide-sun-moon","to":"/docs/getting-started/integrations/color-mode"}] description=Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt. headline=Features title=Beautiful Vue UI components />
</template>
```

Use it after a [PageHero](https://ui.nuxt.com/docs/components/page-hero) component:

```vue {4}
<template>
  <UPageHero />

  <UPageSection />
</template>
```

### Title

Use the `title` prop to set the title of the section.

```vue
<template>
  <UPageSection title="Beautiful Vue UI components" />
</template>
```

### Description

Use the `description` prop to set the description of the section.

```vue
<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." />
</template>
```

### Headline

Use the `headline` prop to set the headline of the section.

```vue
<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." headline="Features" />
</template>
```

### Icon

Use the `icon` prop to set the icon of the section.

```vue
<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." icon="i-lucide-rocket" />
</template>
```

### Features

Use the `features` prop to display a list of [PageFeature](https://ui.nuxt.com/docs/components/page-feature) under the description as an array of objects with the following properties:

- `title?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `orientation?: 'horizontal' | 'vertical'`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

You can pass any property from the [Link](https://ui.nuxt.com/docs/components/link#props) component such as `to`, `target`, etc.

```vue
<script setup lang="ts">
import type { PageFeatureProps } from '@nuxt/ui'
</script>

<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." />
</template>
```

### Links

Use the `links` prop to display a list of [Button](https://ui.nuxt.com/docs/components/button) under the description.

```vue
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation with the default slot. Defaults to `vertical`.

```vue
<script setup lang="ts">
import type { PageFeatureProps } from '@nuxt/ui'
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." icon="i-lucide-rocket" orientation="horizontal">
    <img src="https://picsum.photos/704/1294" width="352" height="647" alt="Illustration" class="w-full rounded-lg" loading="lazy" />
  </UPageSection>
</template>
```

### Reverse

Use the `reverse` prop to reverse the orientation of the default slot.

```vue
<script setup lang="ts">
import type { PageFeatureProps } from '@nuxt/ui'
import type { ButtonProps } from '@nuxt/ui'
</script>

<template>
  <UPageSection title="Beautiful Vue UI components" description="Nuxt UI provides a comprehensive suite of components and utilities to help you build beautiful and accessible web applications with Vue and Nuxt." icon="i-lucide-rocket" orientation="horizontal" reverse>
    <img src="https://picsum.photos/704/1294" width="352" height="647" alt="Illustration" class="w-full rounded-lg" loading="lazy" />
  </UPageSection>
</template>
```

## API

### Props

```ts
/**
 * Props for the PageSection component
 */
interface PageSectionProps {
  /**
   * The element or component this component should render as.
   * @default "\"section\""
   */
  as?: any;
  /**
   * The headline displayed above the title.
   */
  headline?: string | undefined;
  /**
   * The icon displayed above the title.
   */
  icon?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Display a list of Button under the description.
   * `{ size: 'lg' }`{lang="ts-type"}
   */
  links?: ButtonProps[] | undefined;
  /**
   * Display a list of PageFeature under the description.
   */
  features?: PageFeatureProps[] | undefined;
  /**
   * The orientation of the section.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Reverse the order of the default slot.
   */
  reverse?: boolean | undefined;
  ui?: { root?: ClassNameValue; container?: ClassNameValue; wrapper?: ClassNameValue; header?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; headline?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; features?: ClassNameValue; footer?: ClassNameValue; links?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PageSection component
 */
interface PageSectionSlots {
  top(): any;
  header(): any;
  leading(): any;
  headline(): any;
  title(): any;
  description(): any;
  body(): any;
  features(): any;
  footer(): any;
  links(): any;
  default(): any;
  bottom(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pageSection: {
      slots: {
        root: 'relative isolate',
        container: 'flex flex-col lg:grid py-16 sm:py-24 lg:py-32 gap-8 sm:gap-16',
        wrapper: '',
        header: '',
        leading: 'flex items-center mb-6',
        leadingIcon: 'size-10 shrink-0 text-primary',
        headline: 'mb-3',
        title: 'text-3xl sm:text-4xl lg:text-5xl text-pretty tracking-tight font-bold text-highlighted',
        description: 'text-base sm:text-lg text-muted',
        body: 'mt-8',
        features: 'grid',
        footer: 'mt-8',
        links: 'flex flex-wrap gap-x-6 gap-y-3'
      },
      variants: {
        orientation: {
          horizontal: {
            container: 'lg:grid-cols-2 lg:items-center',
            description: 'text-pretty',
            features: 'gap-4'
          },
          vertical: {
            container: '',
            headline: 'justify-center',
            leading: 'justify-center',
            title: 'text-center',
            description: 'text-center text-balance',
            links: 'justify-center',
            features: 'sm:grid-cols-2 lg:grid-cols-3 gap-8'
          }
        },
        reverse: {
          true: {
            wrapper: 'order-last'
          }
        },
        headline: {
          true: {
            headline: 'font-semibold text-primary flex items-center gap-1.5'
          }
        },
        title: {
          true: {
            description: 'mt-6'
          }
        },
        description: {
          true: ''
        },
        body: {
          true: ''
        }
      },
      compoundVariants: [
        {
          orientation: 'vertical',
          title: true,
          class: {
            body: 'mt-16'
          }
        },
        {
          orientation: 'vertical',
          description: true,
          class: {
            body: 'mt-16'
          }
        },
        {
          orientation: 'vertical',
          body: true,
          class: {
            footer: 'mt-16'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PageSection.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/page-section.ts).


# Pagination

## Usage

Use the `default-page` prop or the `v-model:page` directive to control the current page.

```vue
<template>
  <UPagination :page="5" :total="100" />
</template>
```

\> \[!NOTE]
\> The Pagination component uses some \[\`Button\`]\(/docs/components/button) to display the pages, use \[\`color\`]\(#color), \[\`variant\`]\(#variant) and \[\`size\`]\(#size) props to style them.

### Total

Use the `total` prop to set the total number of items in the list.

```vue
<template>
  <UPagination :page="5" :total="100" />
</template>
```

### Items Per Page

Use the `items-per-page` prop to set the number of items per page. Defaults to `10`.

```vue
<template>
  <UPagination :page="5" :items-per-page="20" :total="100" />
</template>
```

### Sibling Count

Use the `sibling-count` prop to set the number of siblings to show. Defaults to `2`.

```vue
<template>
  <UPagination :page="5" :sibling-count="1" :total="100" />
</template>
```

### Show Edges

Use the `show-edges` prop to always show the ellipsis, first and last pages. Defaults to `false`.

```vue
<template>
  <UPagination :page="5" show-edges :sibling-count="1" :total="100" />
</template>
```

### Show Controls

Use the `show-controls` prop to show the first, prev, next and last buttons. Defaults to `true`.

```vue
<template>
  <UPagination :page="5" :show-controls="false" show-edges :total="100" />
</template>
```

### Color

Use the `color` prop to set the color of the inactive controls. Defaults to `neutral`.

```vue
<template>
  <UPagination :page="5" color="primary" :total="100" />
</template>
```

### Variant

Use the `variant` prop to set the variant of the inactive controls. Defaults to `outline`.

```vue
<template>
  <UPagination :page="5" color="neutral" variant="subtle" :total="100" />
</template>
```

### Active Color

Use the `active-color` prop to set the color of the active control. Defaults to `primary`.

```vue
<template>
  <UPagination :page="5" active-color="neutral" :total="100" />
</template>
```

### Active Variant

Use the `active-variant` prop to set the variant of the active control. Defaults to `solid`.

```vue
<template>
  <UPagination :page="5" active-color="primary" active-variant="subtle" :total="100" />
</template>
```

### Size

Use the `size` prop to set the size of the controls. Defaults to `md`.

```vue
<template>
  <UPagination :page="5" size="xl" :total="100" />
</template>
```

### Disabled

Use the `disabled` prop to disable the pagination controls.

```vue
<template>
  <UPagination :page="5" :total="100" disabled />
</template>
```

## Examples

### With links

Use the `to` prop to transform buttons into links. Pass a function that receives the page number and returns a route destination.

```vue [PaginationLinksExample.vue]
<script setup lang="ts">
const page = ref(5)

function to(page: number) {
  return {
    query: {
      page
    },
    hash: '#with-links'
  }
}
</script>

<template>
  <UPagination v-model:page="page" :total="100" :to="to" :sibling-count="1" show-edges />
</template>
```

\> \[!NOTE]
\> In this example we're adding the \`#with-links\` hash to avoid going to the top of the page.

## API

### Props

```ts
/**
 * Props for the Pagination component
 */
interface PaginationProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The icon to use for the first page control.
   */
  firstIcon?: any;
  /**
   * The icon to use for the previous page control.
   */
  prevIcon?: any;
  /**
   * The icon to use for the next page control.
   */
  nextIcon?: any;
  /**
   * The icon to use for the last page control.
   */
  lastIcon?: any;
  /**
   * The icon to use for the ellipsis control.
   */
  ellipsisIcon?: any;
  /**
   * The color of the pagination controls.
   * @default "\"neutral\""
   */
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * The variant of the pagination controls.
   * @default "\"outline\""
   */
  variant?: "outline" | "solid" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * The color of the active pagination control.
   * @default "\"primary\""
   */
  activeColor?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  /**
   * The variant of the active pagination control.
   * @default "\"solid\""
   */
  activeVariant?: "outline" | "solid" | "soft" | "subtle" | "ghost" | "link" | undefined;
  /**
   * Whether to show the first, previous, next, and last controls.
   * @default "true"
   */
  showControls?: boolean | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * A function to render page controls as links.
   */
  to?: ((page: number) => string | St | vt | undefined) | undefined;
  ui?: { root?: ClassNameValue; list?: ClassNameValue; ellipsis?: ClassNameValue; label?: ClassNameValue; first?: ClassNameValue; prev?: ClassNameValue; item?: ClassNameValue; next?: ClassNameValue; last?: ClassNameValue; } | undefined;
  /**
   * The value of the page that should be active when initially rendered.
   * 
   * Use when you do not need to control the value state.
   */
  defaultPage?: number | undefined;
  /**
   * When `true`, prevents the user from interacting with item
   */
  disabled?: boolean | undefined;
  /**
   * Number of items per page
   * @default "10"
   */
  itemsPerPage?: number | undefined;
  /**
   * The controlled value of the current page. Can be binded as `v-model:page`.
   */
  page?: number | undefined;
  /**
   * When `true`, always show first page, last page, and ellipsis
   * @default "false"
   */
  showEdges?: boolean | undefined;
  /**
   * Number of sibling should be shown around the current page
   * @default "2"
   */
  siblingCount?: number | undefined;
  /**
   * Number of items in your list
   * @default "0"
   */
  total?: number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Pagination component
 */
interface PaginationSlots {
  first(): any;
  prev(): any;
  next(): any;
  last(): any;
  ellipsis(): any;
  item(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Pagination component
 */
interface PaginationEmits {
  update:page: (payload: [value: number]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pagination: {
      slots: {
        root: '',
        list: 'flex items-center gap-1',
        ellipsis: 'pointer-events-none',
        label: 'min-w-5 text-center',
        first: '',
        prev: '',
        item: '',
        next: '',
        last: ''
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Pagination.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/pagination.ts).


# PinInput

## Usage

Use the `v-model` directive to control the value of the PinInput.

```vue
<template>
  <UPinInput />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <UPinInput />
</template>
```

### Type

Use the `type` prop to change the input type. Defaults to `text`.

```vue
<template>
  <UPinInput type="number" />
</template>
```

\> \[!NOTE]
\> When \`type\` is set to \`number\`, it will only accept numeric characters.

### Mask

Use the `mask` prop to treat the input like a password.

```vue
<template>
  <UPinInput mask />
</template>
```

### OTP

Use the `otp` prop to enable One-Time Password functionality. When enabled, mobile devices can automatically detect and fill OTP codes from SMS messages or clipboard content, with autocomplete support.

```vue
<template>
  <UPinInput otp />
</template>
```

### Length

Use the `length` prop to change the amount of inputs.

```vue
<template>
  <UPinInput :length="6" />
</template>
```

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<template>
  <UPinInput placeholder="○" />
</template>
```

### Color

Use the `color` prop to change the ring color when the PinInput is focused.

```vue
<template>
  <UPinInput color="neutral" highlight placeholder="○" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the PinInput.

```vue
<template>
  <UPinInput color="neutral" variant="subtle" :highlight="false" placeholder="○" />
</template>
```

### Size

Use the `size` prop to change the size of the PinInput.

```vue
<template>
  <UPinInput size="xl" placeholder="○" />
</template>
```

### Disabled

Use the `disabled` prop to disable the PinInput.

```vue
<template>
  <UPinInput disabled placeholder="○" />
</template>
```

## API

### Props

```ts
/**
 * Props for the PinInput component
 */
interface PinInputProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * The number of input fields.
   * @default "5"
   */
  length?: string | number | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; } | undefined;
  /**
   * The default value of the pin inputs when it is initially rendered. Use when you do not need to control its checked state.
   */
  defaultValue?: PinInputValue<T> | undefined;
  /**
   * When `true`, prevents the user from interacting with the pin input
   */
  disabled?: boolean | undefined;
  /**
   * Id of the element
   */
  id?: string | undefined;
  /**
   * When `true`, pin inputs will be treated as password.
   */
  mask?: boolean | undefined;
  /**
   * The controlled checked state of the pin input. Can be binded as `v-model`.
   */
  modelValue?: PinInputValue<T> | null | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, mobile devices will autodetect the OTP from messages or clipboard, and enable the autocomplete field.
   */
  otp?: boolean | undefined;
  /**
   * The placeholder character to use for empty pin-inputs.
   */
  placeholder?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * Input type for the inputs.
   * @default "\"text\" as never"
   */
  type?: T | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the PinInput component
 */
interface PinInputEmits {
  update:modelValue: (payload: [value: PinInputValue<T>]) => void;
  complete: (payload: [value: PinInputValue<T>]) => void;
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: Event]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                            | Type                                                                                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputsRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pinInput: {
      slots: {
        root: 'relative inline-flex items-center gap-1.5',
        base: [
          'rounded-md border-0 placeholder:text-dimmed text-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ]
      },
      variants: {
        size: {
          xs: {
            base: 'size-6 text-sm/4'
          },
          sm: {
            base: 'size-7 text-sm/4'
          },
          md: {
            base: 'size-8 text-base/5'
          },
          lg: {
            base: 'size-9 text-base/5'
          },
          xl: {
            base: 'size-10 text-base'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PinInput.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/pin-input.ts).


# Popover

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Popover.

Then, use the `#content` slot to add the content displayed when the Popover is open.

```vue
<template>
  <UPopover>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

### Mode

Use the `mode` prop to change the mode of the Popover. Defaults to `click`.

```vue
<template>
  <UPopover mode="hover">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

\> \[!NOTE]
\> When using the \`hover\` mode, the Reka UI \[\`HoverCard\`]\(https\://reka-ui.com/docs/components/hover-card) component is used instead of the \[\`Popover\`]\(https\://reka-ui.com/docs/components/popover).

### Delay

When using the `hover` mode, you can use the `open-delay` and `close-delay` props to control the delay before the Popover is opened or closed.

```vue
<template>
  <UPopover mode="hover" :open-delay="500" :close-delay="300">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

### Content

Use the `content` prop to control how the Popover content is rendered, like its `align` or `side` for example.

```vue
<template>
  <UPopover>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

### Arrow

Use the `arrow` prop to display an arrow on the Popover.

```vue
<template>
  <UPopover arrow>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

### Modal

Use the `modal` prop to control whether the Popover blocks interaction with outside content. Defaults to `false`.

```vue
<template>
  <UPopover modal>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template></UPopover>
</template>
```

### Dismissible

Use the `dismissible` prop to control whether the Popover is dismissible when clicking outside of it or pressing escape. Defaults to `true`.

\> \[!NOTE]
\> A \`close\:prevent\` event will be emitted when the user tries to close it.

```vue [PopoverDismissibleExample.vue]
<template>
  <UPopover :dismissible="false" :ui="{ content: 'p-4' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content="{ close }">
      <div class="flex items-center gap-4 mb-4">
        <h2 class="text-highlighted font-semibold">
          Popover non-dismissible
        </h2>

        <UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="close" />
      </div>

      <Placeholder class="size-full min-h-48" />
    </template>
  </UPopover>
</template>
```

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [PopoverOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UPopover v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </UPopover>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Popover by pressing .

### With command palette

You can use a [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) component inside the Popover's content.

```vue [PopoverCommandPaletteExample.vue]
<script setup lang="ts">
import type { CommandPaletteItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error'
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success'
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info'
    }
  }
] satisfies CommandPaletteItem[])

const label = ref([])
</script>

<template>
  <UPopover :content="{ side: 'right', align: 'start' }">
    <UButton
      icon="i-lucide-tag"
      label="Select labels"
      color="neutral"
      variant="subtle"
    />

    <template #content>
      <UCommandPalette
        v-model="label"
        multiple
        placeholder="Search labels..."
        :groups="[{ id: 'labels', items }]"
        :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }"
      />
    </template>
  </UPopover>
</template>
```

### With following cursor

You can make the Popover follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger){rel="&#x22;nofollow&#x22;"} prop:

```vue [PopoverCursorExample.vue]
<script setup lang="ts">
const open = ref(false)
const anchor = ref({ x: 0, y: 0 })

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value
    } as DOMRect)
}))
</script>

<template>
  <UPopover
    :open="open"
    :reference="reference"
    :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
      @pointerenter="open = true"
      @pointerleave="open = false"
      @pointermove="(ev: PointerEvent) => {
        anchor.x = ev.clientX
        anchor.y = ev.clientY
      }"
    >
      Hover me
    </div>

    <template #content>
      <div class="p-4">
        {{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
      </div>
    </template>
  </UPopover>
</template>
```

### With anchor slot

You can use the `#anchor` slot to position the Popover against a custom element.

\> \[!WARNING]
\> This slot only works when \`mode\` is \`click\`.

```vue [PopoverAnchorSlotExample.vue]
<script lang="ts" setup>
const open = ref(false)
</script>

<template>
  <UPopover
    v-model:open="open"
    :dismissible="false"
    :ui="{ content: 'w-(--reka-popper-anchor-width) p-4' }"
  >
    <template #anchor>
      <UInput placeholder="Focus to open" @focus="open = true" @blur="open = false" />
    </template>

    <template #content>
      <Placeholder class="w-full aspect-square" />
    </template>
  </UPopover>
</template>
```

## API

### Props

```ts
/**
 * Props for the Popover component
 */
interface PopoverProps {
  /**
   * The display mode of the popover.
   * @default "\"click\" as never"
   */
  mode?: M | undefined;
  /**
   * The content of the popover.
   */
  content?: (Omit<PopoverContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<PopoverContentImplEmits>>) | undefined;
  /**
   * Display an arrow alongside the popover.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<PopoverArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the popover in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * The reference (or anchor) element that is being referred to for positioning.
   * 
   * If not provided will use the current component as anchor.
   */
  reference?: ReferenceElement | undefined;
  /**
   * When `false`, the popover will not close when clicking outside or pressing escape.
   * @default "true"
   */
  dismissible?: boolean | undefined;
  ui?: { content?: ClassNameValue; arrow?: ClassNameValue; } | undefined;
  /**
   * The open state of the popover when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the popover.
   */
  open?: boolean | undefined;
  /**
   * The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.
   */
  modal?: boolean | undefined;
  /**
   * The duration from when the mouse enters the trigger until the hover card opens.
   * @default "0"
   */
  openDelay?: number | undefined;
  /**
   * The duration from when the mouse leaves the trigger or content until the hover card closes.
   * @default "0"
   */
  closeDelay?: number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Popover component
 */
interface PopoverSlots {
  default(): any;
  content(): any;
  anchor(): any;
}
```

\> \[!NOTE]
\> The \`close\` function is only available when \`mode\` is set to \`click\` because Reka UI exposes this for \[\`Popover\`]\(https\://reka-ui.com/docs/components/popover#close-using-slot-props) but not for \[\`HoverCard\`]\(https\://reka-ui.com/docs/components/hover-card).

### Emits

```ts
/**
 * Emitted events for the Popover component
 */
interface PopoverEmits {
  close:prevent: (payload: []) => void;
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    popover: {
      slots: {
        content: 'bg-default shadow-lg rounded-md ring ring-default data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-popover-content-transform-origin) focus:outline-none pointer-events-auto',
        arrow: 'fill-bg stroke-default'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Popover.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/popover.ts).


# PricingPlan

## Usage

The PricingPlan component provides a flexible way to display a pricing plan with customizable content including title, description, price, features, etc.

```vue
<template>
  <u-pricing-plan :button={"label":"Buy now"} :features=["One developer","Unlimited projects","Access to GitHub repository","Unlimited patch & minor updates","Lifetime access"] badge=Most popular billing-cycle=/month description=For bootstrappers and indie hackers. discount=$199 price=$249 title=Solo />
</template>
```

\> \[!TIP]
\> See: /docs/components/pricing-plans
\> Use the \`PricingPlans\` component to display multiple pricing plans in a responsive grid layout.

### Title

Use the `title` prop to set the title of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" class="w-96" />
</template>
```

### Description

Use the `description` prop to set the description of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." />
</template>
```

### Badge

Use the `badge` prop to display a [Badge](https://ui.nuxt.com/docs/components/badge) next to the title of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." badge="Most popular" />
</template>
```

You can pass any property from the [Badge](https://ui.nuxt.com/docs/components/badge#props) component to customize it.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." />
</template>
```

### Price

Use the `price` prop to set the price of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" />
</template>
```

### Discount

Use the `discount` prop to set a discounted price that will be displayed alongside the original price (which will be shown with a strikethrough).

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" discount="$199" />
</template>
```

### Billing

Use the `billing-cycle` and/or `billing-period` props to display the billing information of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$9" billing-cycle="/month" billing-period="billed annually" />
</template>
```

### Features

Use the `features` prop as an array of string to display a list of features on the PricingPlan:

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.success\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.success\` key.

You can also pass an array of objects with the following properties:

- `title: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { PricingPlanFeature } from '@nuxt/ui'
</script>

<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" />
</template>
```

### Button

Use the `button` prop with any property from the [Button](https://ui.nuxt.com/docs/components/button) component to display a button at the bottom of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" />
</template>
```

\> \[!TIP]
\> Use the \`onClick\` field to add a click handler to trigger the plan purchase.

### Variant

Use the `variant` prop to change the variant of the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" variant="subtle" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the PricingPlan. Defaults to `vertical`.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" orientation="horizontal" variant="outline" />
</template>
```

### Tagline

Use the `tagline` prop to display a tagline text above the price.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" orientation="horizontal" tagline="Pay once, own it forever" />
</template>
```

### Terms

Use the `terms` prop to display terms below the price.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" orientation="horizontal" tagline="Pay once, own it forever" terms="Invoices and receipts available." />
</template>
```

### Highlight

Use the `highlight` prop to display a highlighted border around the PricingPlan.

```vue
<template>
  <UPricingPlan title="Solo" description="For bootstrappers and indie hackers." price="$249" highlight />
</template>
```

### Scale

Use the `scale` prop to make a PricingPlan bigger than the others.

\> \[!NOTE]
\> See: /docs/components/pricing-plans#scale
\> Check out the PricingPlans's \`scale\` example to see how it works as it's hard to demonstrate by itself.

## API

### Props

```ts
/**
 * Props for the PricingPlan component
 */
interface PricingPlanProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Display a badge next to the title.
   * Can be a string or an object.
   * `{ color: 'primary', variant: 'subtle' }`{lang="ts-type"}
   */
  badge?: string | BadgeProps | undefined;
  /**
   * The unit price period that appears next to the price.
   * Typically used to show the recurring interval.
   */
  billingCycle?: string | undefined;
  /**
   * Additional billing context that appears above the billing cycle.
   * Typically used to show the actual billing frequency.
   */
  billingPeriod?: string | undefined;
  /**
   * The current price of the plan.
   * When used with `discount`, this becomes the original price.
   */
  price?: string | undefined;
  /**
   * The discounted price of the plan.
   * When provided, the `price` prop will be displayed as strikethrough.
   */
  discount?: string | undefined;
  /**
   * Display a list of features under the price.
   * Can be an array of strings or an array of objects.
   */
  features?: string[] | PricingPlanFeature[] | undefined;
  /**
   * Display a buy button at the bottom.
   * `{ size: 'lg', block: true }`{lang="ts-type"}
   * Use the `onClick` field to add a click handler.
   */
  button?: ButtonProps | undefined;
  /**
   * Display a tagline highlighting the pricing value proposition.
   */
  tagline?: string | undefined;
  /**
   * Display terms at the bottom.
   */
  terms?: string | undefined;
  /**
   * The orientation of the pricing plan.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  variant?: "soft" | "solid" | "outline" | "subtle" | undefined;
  /**
   * Display a ring around the pricing plan to highlight it.
   */
  highlight?: boolean | undefined;
  /**
   * Enlarge the plan to make it more prominent.
   */
  scale?: boolean | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; titleWrapper?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; priceWrapper?: ClassNameValue; price?: ClassNameValue; discount?: ClassNameValue; billing?: ClassNameValue; billingPeriod?: ClassNameValue; billingCycle?: ClassNameValue; features?: ClassNameValue; feature?: ClassNameValue; featureIcon?: ClassNameValue; featureTitle?: ClassNameValue; badge?: ClassNameValue; button?: ClassNameValue; tagline?: ClassNameValue; terms?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PricingPlan component
 */
interface PricingPlanSlots {
  badge(): any;
  title(): any;
  description(): any;
  price(): any;
  discount(): any;
  billing(): any;
  features(): any;
  button(): any;
  header(): any;
  body(): any;
  footer(): any;
  tagline(): any;
  terms(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pricingPlan: {
      slots: {
        root: 'relative grid rounded-lg p-6 lg:p-8 xl:p-10 gap-6',
        header: '',
        body: 'flex flex-col min-w-0',
        footer: 'flex flex-col gap-6 items-center',
        titleWrapper: 'flex items-center gap-3',
        title: 'text-highlighted truncate text-2xl sm:text-3xl text-pretty font-semibold',
        description: 'text-muted text-base text-pretty mt-2',
        priceWrapper: 'flex items-center gap-1',
        price: 'text-highlighted text-3xl sm:text-4xl font-semibold',
        discount: 'text-muted line-through text-xl sm:text-2xl',
        billing: 'flex flex-col justify-between min-w-0',
        billingPeriod: 'text-toned truncate text-xs font-medium',
        billingCycle: 'text-muted truncate text-xs font-medium',
        features: 'flex flex-col gap-3 flex-1 mt-6 grow-0',
        feature: 'flex items-center gap-2 min-w-0',
        featureIcon: 'size-5 shrink-0 text-primary',
        featureTitle: 'text-muted text-sm truncate',
        badge: '',
        button: '',
        tagline: 'text-base font-semibold text-default',
        terms: 'text-xs/5 text-muted text-center text-balance'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'grid-cols-1 lg:grid-cols-3 justify-between divide-y lg:divide-y-0 lg:divide-x divide-default',
            body: 'lg:col-span-2 pb-6 lg:pb-0 lg:pr-6 justify-center',
            footer: 'lg:justify-center lg:items-center lg:p-6 lg:max-w-xs lg:w-full lg:mx-auto',
            features: 'lg:grid lg:grid-cols-2 lg:mt-12'
          },
          vertical: {
            footer: 'justify-end',
            priceWrapper: 'mt-6'
          }
        },
        variant: {
          solid: {
            root: 'bg-inverted',
            title: 'text-inverted',
            description: 'text-dimmed',
            price: 'text-inverted',
            discount: 'text-dimmed',
            billingCycle: 'text-dimmed',
            billingPeriod: 'text-dimmed',
            featureTitle: 'text-dimmed'
          },
          outline: {
            root: 'bg-default ring ring-default'
          },
          soft: {
            root: 'bg-elevated/50'
          },
          subtle: {
            root: 'bg-elevated/50 ring ring-default'
          }
        },
        highlight: {
          true: {
            root: 'ring-2 ring-inset ring-primary'
          }
        },
        scale: {
          true: {
            root: 'lg:scale-[1.1] lg:z-[1]'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          variant: 'soft',
          class: {
            root: 'divide-accented'
          }
        },
        {
          orientation: 'horizontal',
          variant: 'subtle',
          class: {
            root: 'divide-accented'
          }
        }
      ],
      defaultVariants: {
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PricingPlan.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/pricing-plan.ts).


# PricingPlans

## Usage

The PricingPlans component provides a flexible layout to display a list of [PricingPlan](https://ui.nuxt.com/docs/components/pricing-plan) components using either the default slot or the `plans` prop.

```vue {2,8}
<template>
  <UPricingPlans>
    <UPricingPlan
      v-for="(plan, index) in plans"
      :key="index"
      v-bind="plan"
    />
  </UPricingPlans>
</template>
```

\> \[!TIP]
\> The grid columns will be automatically calculated based on the number of plans, this works with the \`plans\` prop but also with the default slot.

### Plans

Use the `plans` prop as an array of objects with the properties of the [PricingPlan](https://ui.nuxt.com/docs/components/pricing-plan#props) component.

```vue
<script setup lang="ts">
import type { PricingPlanProps } from '@nuxt/ui'
</script>

<template>
  <UPricingPlans />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the PricingPlans. Defaults to `horizontal`.

```vue
<script setup lang="ts">
import type { PricingPlanProps } from '@nuxt/ui'
</script>

<template>
  <UPricingPlans orientation="vertical" />
</template>
```

\> \[!TIP]
\> When using the \`plans\` prop instead of the default slot, the \`orientation\` of the plans is automatically reversed, \`horizontal\` to \`vertical\` and vice versa.

### Compact

Use the `compact` prop to reduce the padding between the plans when one of the plans is scaled for a better visual balance.

```vue
<script setup lang="ts">
import type { PricingPlanProps } from '@nuxt/ui'
</script>

<template>
  <UPricingPlans compact />
</template>
```

### Scale

Use the `scale` prop to adjust the spacing between the plans when one of the plans is scaled for a better visual balance.

```vue
<script setup lang="ts">
import type { PricingPlanProps } from '@nuxt/ui'
</script>

<template>
  <UPricingPlans scale />
</template>
```

## Examples

\> \[!NOTE]
\> While these examples use \[Nuxt Content]\(https\://content.nuxt.com), the components can be integrated with any content management system.

### Within a page

Use the PricingPlans component in a page to create a pricing page:

```vue [pages/pricing/index.vue] {11}
<script setup lang="ts">
const { data: plans } = await useAsyncData('plans', () => queryCollection('plans').all())
</script>

<template>
  <UPage>
    <UPageHero title="Pricing" />

    <UPageBody>
      <UContainer>
        <UPricingPlans :plans="plans" />
      </UContainer>
    </UPageBody>
  </UPage>
</template>
```

\> \[!NOTE]
\> In this example, the \`plans\` are fetched using \`queryCollection\` from the \`@nuxt/content\` module.

## API

### Props

```ts
/**
 * Props for the PricingPlans component
 */
interface PricingPlansProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  plans?: PricingPlanProps[] | undefined;
  /**
   * The orientation of the pricing plans.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * When `true`, the plans will be displayed without gap.
   * @default "false"
   */
  compact?: boolean | undefined;
  /**
   * When `true`, the plans will be displayed with a larger gap.
   * Useful when one plan is scaled. Doesn't work with `compact`.
   * @default "false"
   */
  scale?: boolean | undefined;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PricingPlans component
 */
interface PricingPlansSlots {
  badge(): any;
  title(): any;
  description(): any;
  price(): any;
  discount(): any;
  billing(): any;
  features(): any;
  button(): any;
  header(): any;
  body(): any;
  footer(): any;
  tagline(): any;
  terms(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pricingPlans: {
      base: 'flex flex-col gap-y-8',
      variants: {
        orientation: {
          horizontal: 'lg:grid lg:grid-cols-[repeat(var(--count),minmax(0,1fr))]',
          vertical: ''
        },
        compact: {
          false: 'gap-x-8'
        },
        scale: {
          true: ''
        }
      },
      compoundVariants: [
        {
          compact: false,
          scale: true,
          class: 'lg:gap-x-13'
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PricingPlans.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/pricing-plans.ts).


# PricingTable

## Usage

The PricingTable component provides a responsive and customizable way to display pricing plans in a table format, automatically switching between a horizontal table layout on desktop for easy comparison and a vertical card layout on mobile for better readability.

```vue
<template>
  <u-pricing-table :sections=[{"title":"Features","features":[{"title":"Number of developers","tiers":{"solo":"1","team":"5","enterprise":"Unlimited"}},{"title":"Projects","tiers":{"solo":true,"team":true,"enterprise":true}},{"title":"GitHub repository access","tiers":{"solo":true,"team":true,"enterprise":true}},{"title":"Updates","tiers":{"solo":"Patch & minor","team":"All updates","enterprise":"All updates"}},{"title":"Support","tiers":{"solo":"Community","team":"Priority","enterprise":"24/7"}}]},{"title":"Security","features":[{"title":"SSO","tiers":{"solo":false,"team":true,"enterprise":true}},{"title":"Audit logs","tiers":{"solo":false,"team":true,"enterprise":true}},{"title":"Custom security review","tiers":{"solo":false,"team":false,"enterprise":true}}]}] :tiers=[{"id":"solo","title":"Solo","description":"For indie hackers.","price":"$249","billingCycle":"/month","billingPeriod":"billed annually","badge":"Most popular","button":{"label":"Buy now","variant":"subtle"}},{"id":"team","title":"Team","description":"For growing teams.","price":"$499","billingCycle":"/month","billingPeriod":"billed annually","button":{"label":"Buy now"},"highlight":true},{"id":"enterprise","title":"Enterprise","description":"For large organizations.","price":"Custom","button":{"label":"Contact sales","color":"neutral"}}] />
</template>
```

### Tiers

Use the `tiers` prop as an array of objects to define your pricing plans. Each tier object supports the following properties:

- `id: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Unique identifier for the tier (required)
- `title?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Name of the pricing plan
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Short description of the plan
- `price?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - The current price of the plan (e.g., "$99", "€99", "Free")
- `discount?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - The discounted price that will display the `price` with strikethrough (e.g., "$79", "€79")
- `billingCycle?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - The unit price period that appears next to the price (e.g., "/month", "/seat/month")
- `billingPeriod?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Additional billing context that appears above the billing cycle (e.g., "billed monthly")
- `badge?: string | BadgeProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Display a badge next to the title `{ color: 'primary', variant: 'subtle' }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `button?: ButtonProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Configure the CTA button `{ size: 'lg', block: true }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `highlight?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - Whether to visually emphasize this tier as the recommended option

```vue
<script setup lang="ts">
import type { PricingTableTier } from '@nuxt/ui'
</script>

<template>
  <UPricingTable />
</template>
```

### Sections

Use the `sections` prop to organize features into logical groups. Each section represents a category of features that you want to compare across different pricing tiers.

- `title: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} - The heading for the feature section
- `features: PricingTableSectionFeature[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}- An array of features with their availability in each tier:
  - Each feature requires a `title` and a `tiers` object mapping tier IDs to values
  - Boolean values (`true`/`false`) will display as checkmarks (✓) or minus icons (-)
  - String values will be shown as text (e.g., "Unlimited", "Up to 5 users")
  - Numeric values will be displayed as is (e.g., 10, 100)

```vue
<script setup lang="ts">
import type { PricingTableTier } from '@nuxt/ui'
import type { PricingTableSection } from '@nuxt/ui'
</script>

<template>
  <UPricingTable />
</template>
```

## Examples

### With slots

The PricingTable component provides powerful slot customization options to tailor the display of your content. You can customize individual elements using generic slots or target specific items using their IDs.

```vue [PricingTableSlotsExample.vue]
<script setup lang="ts">
const tiers = [
  {
    id: 'solo',
    title: 'Solo',
    price: '$249',
    description: 'For indie hackers.',
    billingCycle: '/month',
    button: { label: 'Buy now', variant: 'subtle' as const }
  },
  {
    id: 'team',
    title: 'Team',
    price: '$499',
    description: 'For growing teams.',
    billingCycle: '/month',
    button: { label: 'Buy now' },
    highlight: true
  },
  {
    id: 'enterprise',
    title: 'Enterprise',
    price: 'Custom',
    description: 'For large organizations.',
    button: { label: 'Contact sales', color: 'neutral' as const }
  }
]

const sections = [
  {
    id: 'features',
    title: 'Features',
    features: [
      {
        id: 'developers',
        title: 'Number of developers',
        tiers: { solo: '1', team: '5', enterprise: 'Unlimited' }
      },
      {
        id: 'projects',
        title: 'Projects',
        tiers: { solo: true, team: true, enterprise: true }
      }
    ]
  },
  {
    id: 'security',
    title: 'Security',
    features: [
      {
        title: 'SSO',
        tiers: { solo: false, team: true, enterprise: true }
      }
    ]
  }
]
</script>

<template>
  <UPricingTable :tiers="tiers" :sections="sections">
    <!-- Customize specific tier title -->
    <template #team-title="{ tier }">
      <div class="flex items-center gap-2">
        <UIcon name="i-lucide-crown" class="size-4 text-amber-500" />
        {{ tier.title }}
      </div>
    </template>

    <!-- Customize specific section title -->
    <template #section-security-title="{ section }">
      <div class="flex items-center gap-2">
        <UIcon name="i-lucide-shield-check" class="size-4 text-green-500" />
        <span class="font-semibold text-green-700">{{ section.title }}</span>
      </div>
    </template>

    <!-- Customize specific feature value -->
    <template #feature-developers-value="{ feature, tier }">
      <template v-if="feature.tiers?.[tier.id]">
        <UBadge :label="String(feature.tiers[tier.id])" color="primary" variant="soft" />
      </template>
      <UIcon v-else name="i-lucide-x" class="size-4 text-muted" />
    </template>
  </UPricingTable>
</template>
```

The component supports various slot types for maximum customization flexibility:

| Slot Type         | Pattern                                       | Description              | Example                      |
| ----------------- | --------------------------------------------- | ------------------------ | ---------------------------- |
| **Tier slots**    | `#{tier-id}-{element}`                        | Target specific tiers    | `#team-title`, `#solo-price` |
| **Section slots** | `#section-{id|formatted-title}-title`         | Target specific sections | `#section-features-title`    |
| **Feature slots** | `#feature-{id|formatted-title}-{title|value}` | Target specific features | `#feature-developers-title`  |
| **Generic slots** | `#tier-title`, `#section-title`, etc.         | Apply to all items       | `#feature-value`             |

\> \[!NOTE]
\> When no \`id\` is provided, the slot name is auto-generated from the title (e.g., "Premium Features!" becomes \`#section-premium-features-title\`).

## API

### Props

```ts
/**
 * Props for the PricingTable component
 */
interface PricingTableProps {
  /**
   * The pricing tiers to display in the table.
   * Each tier represents a pricing plan with its own title, description, price, and features.
   */
  tiers: T[];
  /**
   * The sections of features to display in the table.
   * Each section contains a title and a list of features with their availability in each tier.
   */
  sections: PricingTableSection<T>[];
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The caption to display above the table.
   */
  caption?: string | undefined;
  ui?: { root?: ClassNameValue; table?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; caption?: ClassNameValue; thead?: ClassNameValue; tbody?: ClassNameValue; tr?: ClassNameValue; th?: ClassNameValue; td?: ClassNameValue; tier?: ClassNameValue; tierTitleWrapper?: ClassNameValue; tierTitle?: ClassNameValue; tierDescription?: ClassNameValue; tierBadge?: ClassNameValue; tierPriceWrapper?: ClassNameValue; tierPrice?: ClassNameValue; tierDiscount?: ClassNameValue; tierBilling?: ClassNameValue; tierBillingPeriod?: ClassNameValue; tierBillingCycle?: ClassNameValue; tierButton?: ClassNameValue; tierFeatureIcon?: ClassNameValue; section?: ClassNameValue; sectionTitle?: ClassNameValue; feature?: ClassNameValue; featureTitle?: ClassNameValue; featureValue?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the PricingTable component
 */
interface PricingTableSlots {
  caption(): any;
  tier(): any;
  tier-title(): any;
  tier-description(): any;
  tier-badge(): any;
  tier-button(): any;
  tier-billing(): any;
  tier-discount(): any;
  tier-price(): any;
  section-title(): any;
  feature-title(): any;
  feature-value(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    pricingTable: {
      slots: {
        root: 'w-full relative',
        table: 'w-full table-fixed border-separate border-spacing-x-0 hidden md:table',
        list: 'md:hidden flex flex-col gap-6 w-full',
        item: 'p-6 flex flex-col border border-default rounded-lg',
        caption: 'sr-only',
        thead: '',
        tbody: '',
        tr: '',
        th: 'py-4 font-normal text-left border-b border-default',
        td: 'px-6 py-4 text-center border-b border-default',
        tier: 'p-6 text-left font-normal',
        tierTitleWrapper: 'flex items-center gap-3',
        tierTitle: 'text-lg font-semibold text-highlighted',
        tierDescription: 'text-sm font-normal text-muted mt-1',
        tierBadge: 'truncate',
        tierPriceWrapper: 'flex items-center gap-1 mt-4',
        tierPrice: 'text-highlighted text-3xl sm:text-4xl font-semibold',
        tierDiscount: 'text-muted line-through text-xl sm:text-2xl',
        tierBilling: 'flex flex-col justify-between min-w-0',
        tierBillingPeriod: 'text-toned truncate text-xs font-medium',
        tierBillingCycle: 'text-muted truncate text-xs font-medium',
        tierButton: 'mt-6',
        tierFeatureIcon: 'size-5 shrink-0',
        section: 'mt-6 flex flex-col gap-2',
        sectionTitle: 'font-semibold text-sm text-highlighted',
        feature: 'flex items-center justify-between gap-1',
        featureTitle: 'text-sm text-default',
        featureValue: 'text-sm text-muted flex justify-center min-w-5'
      },
      variants: {
        section: {
          true: {
            tr: '*:pt-8'
          }
        },
        active: {
          true: {
            tierFeatureIcon: 'text-primary'
          }
        },
        highlight: {
          true: {
            tier: 'bg-elevated/50 border-x border-t border-default rounded-t-lg',
            td: 'bg-elevated/50 border-x border-default',
            item: 'bg-elevated/50'
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/PricingTable.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/pricing-table.ts).


# Progress

## Usage

Use the `v-model` directive to control the value of the Progress.

```vue
<template>
  <UProgress :model-value="50" />
</template>
```

### Max

Use the `max` prop to set the maximum value of the Progress.

```vue
<template>
  <UProgress :model-value="3" :max="4" />
</template>
```

Use the `max` prop with an array of strings to display the active step under the bar, the maximum value of the Progress is the length of the array.

```vue
<template>
  <UProgress :model-value="3" />
</template>
```

### Status

Use the `status` prop to display the current Progress value above the bar.

```vue
<template>
  <UProgress :model-value="50" status />
</template>
```

### Indeterminate

When no `v-model` is set or the value is `null`, the Progress becomes *indeterminate*. The progress bar is animated as a `carousel`, but you can change it using the [`animation`](https://ui.nuxt.com/#animation) prop.

```vue
<template>
  <UProgress />
</template>
```

### Animation

Use the `animation` prop to change the animation of the Progress to an inverse carousel, a swinging bar or an elastic bar. Defaults to `carousel`.

```vue
<template>
  <UProgress animation="swing" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Progress. Defaults to `horizontal`.

```vue
<template>
  <UProgress orientation="vertical" class="h-48" />
</template>
```

### Color

Use the `color` prop to change the color of the Slider.

```vue
<template>
  <UProgress color="neutral" />
</template>
```

### Size

Use the `size` prop to change the size of the Slider.

```vue
<template>
  <UProgress size="xl" />
</template>
```

### Inverted

Use the `inverted` prop to visually invert the Progress.

```vue
<template>
  <UProgress inverted :model-value="25" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Progress component
 */
interface ProgressProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The maximum progress value.
   */
  max?: number | any[] | undefined;
  /**
   * Display the current progress value.
   */
  status?: boolean | undefined;
  /**
   * Whether the progress is visually inverted.
   * @default "false"
   */
  inverted?: boolean | undefined;
  size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The orientation of the progress bar.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * The animation of the progress bar.
   */
  animation?: "carousel" | "carousel-inverse" | "swing" | "elastic" | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; indicator?: ClassNameValue; status?: ClassNameValue; steps?: ClassNameValue; step?: ClassNameValue; } | undefined;
  /**
   * A function to get the accessible label text in a human-readable format.
   * 
   *  If not provided, the value label will be read as the numeric value as a percentage of the max value.
   */
  getValueLabel?: ((value: number | null | undefined, max: number) => string | undefined) | undefined;
  /**
   * A function to get the accessible value text representing the current value in a human-readable format.
   */
  getValueText?: ((value: number | null | undefined, max: number) => string | undefined) | undefined;
  /**
   * The progress value. Can be bind as `v-model`.
   * @default "null"
   */
  modelValue?: number | null | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Progress component
 */
interface ProgressSlots {
  status(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Progress component
 */
interface ProgressEmits {
  update:modelValue: (payload: [value: string[] | undefined]) => void;
  update:max: (payload: [value: number]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    progress: {
      slots: {
        root: 'gap-2',
        base: 'relative overflow-hidden rounded-full bg-accented',
        indicator: 'rounded-full size-full transition-transform duration-200 ease-out',
        status: 'flex text-dimmed transition-[width] duration-200',
        steps: 'grid items-end',
        step: 'truncate text-end row-start-1 col-start-1 transition-opacity'
      },
      variants: {
        animation: {
          carousel: '',
          'carousel-inverse': '',
          swing: '',
          elastic: ''
        },
        color: {
          primary: {
            indicator: 'bg-primary',
            steps: 'text-primary'
          },
          secondary: {
            indicator: 'bg-secondary',
            steps: 'text-secondary'
          },
          success: {
            indicator: 'bg-success',
            steps: 'text-success'
          },
          info: {
            indicator: 'bg-info',
            steps: 'text-info'
          },
          warning: {
            indicator: 'bg-warning',
            steps: 'text-warning'
          },
          error: {
            indicator: 'bg-error',
            steps: 'text-error'
          },
          neutral: {
            indicator: 'bg-inverted',
            steps: 'text-inverted'
          }
        },
        size: {
          '2xs': {
            status: 'text-xs',
            steps: 'text-xs'
          },
          xs: {
            status: 'text-xs',
            steps: 'text-xs'
          },
          sm: {
            status: 'text-sm',
            steps: 'text-sm'
          },
          md: {
            status: 'text-sm',
            steps: 'text-sm'
          },
          lg: {
            status: 'text-sm',
            steps: 'text-sm'
          },
          xl: {
            status: 'text-base',
            steps: 'text-base'
          },
          '2xl': {
            status: 'text-base',
            steps: 'text-base'
          }
        },
        step: {
          active: {
            step: 'opacity-100'
          },
          first: {
            step: 'opacity-100 text-muted'
          },
          other: {
            step: 'opacity-0'
          },
          last: {
            step: ''
          }
        },
        orientation: {
          horizontal: {
            root: 'w-full flex flex-col',
            base: 'w-full',
            status: 'flex-row items-center justify-end min-w-fit'
          },
          vertical: {
            root: 'h-full flex flex-row-reverse',
            base: 'h-full',
            status: 'flex-col justify-end min-h-fit'
          }
        },
        inverted: {
          true: {
            status: 'self-end'
          }
        }
      },
      compoundVariants: [
        {
          inverted: true,
          orientation: 'horizontal',
          class: {
            step: 'text-start',
            status: 'flex-row-reverse'
          }
        },
        {
          inverted: true,
          orientation: 'vertical',
          class: {
            steps: 'items-start',
            status: 'flex-col-reverse'
          }
        },
        {
          orientation: 'horizontal',
          size: '2xs',
          class: 'h-px'
        },
        {
          orientation: 'horizontal',
          size: 'xs',
          class: 'h-0.5'
        },
        {
          orientation: 'horizontal',
          size: 'sm',
          class: 'h-1'
        },
        {
          orientation: 'horizontal',
          size: 'md',
          class: 'h-2'
        },
        {
          orientation: 'horizontal',
          size: 'lg',
          class: 'h-3'
        },
        {
          orientation: 'horizontal',
          size: 'xl',
          class: 'h-4'
        },
        {
          orientation: 'horizontal',
          size: '2xl',
          class: 'h-5'
        },
        {
          orientation: 'vertical',
          size: '2xs',
          class: 'w-px'
        },
        {
          orientation: 'vertical',
          size: 'xs',
          class: 'w-0.5'
        },
        {
          orientation: 'vertical',
          size: 'sm',
          class: 'w-1'
        },
        {
          orientation: 'vertical',
          size: 'md',
          class: 'w-2'
        },
        {
          orientation: 'vertical',
          size: 'lg',
          class: 'w-3'
        },
        {
          orientation: 'vertical',
          size: 'xl',
          class: 'w-4'
        },
        {
          orientation: 'vertical',
          size: '2xl',
          class: 'w-5'
        },
        {
          orientation: 'horizontal',
          animation: 'carousel',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[carousel_2s_ease-in-out_infinite] data-[state=indeterminate]:rtl:animate-[carousel-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'vertical',
          animation: 'carousel',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[carousel-vertical_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'horizontal',
          animation: 'carousel-inverse',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[carousel-inverse_2s_ease-in-out_infinite] data-[state=indeterminate]:rtl:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'vertical',
          animation: 'carousel-inverse',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[carousel-inverse-vertical_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'horizontal',
          animation: 'swing',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[swing_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'vertical',
          animation: 'swing',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[swing-vertical_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'horizontal',
          animation: 'elastic',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[elastic_2s_ease-in-out_infinite]'
          }
        },
        {
          orientation: 'vertical',
          animation: 'elastic',
          class: {
            indicator: 'data-[state=indeterminate]:animate-[elastic-vertical_2s_ease-in-out_infinite]'
          }
        }
      ],
      defaultVariants: {
        animation: 'carousel',
        color: 'primary',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Progress.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/progress.ts).


# RadioGroup

## Usage

Use the `v-model` directive to control the value of the RadioGroup or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup model-value="System" :items="items" />
</template>
```

### Items

Use the `items` prop as an array of strings or numbers:

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup model-value="System" :items="items" />
</template>
```

You can also pass an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`value?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#value-key)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { RadioGroupItem } from '@nuxt/ui'

const items = ref<RadioGroupItem[]>([
  {
    label: 'System',
    description: 'This is the first option.',
    value: 'system',
  },
  {
    label: 'Light',
    description: 'This is the second option.',
    value: 'light',
  },
  {
    label: 'Dark',
    description: 'This is the third option.',
    value: 'dark',
  },
])
</script>

<template>
  <URadioGroup model-value="system" :items="items" />
</template>
```

\> \[!CAUTION]
\> When using objects, you need to reference the \`value\` property of the object in the \`v-model\` directive or the \`default-value\` prop.

### Value Key

You can change the property that is used to set the value by using the `value-key` prop. Defaults to `value`.

```vue
<script setup lang="ts">
import type { RadioGroupItem } from '@nuxt/ui'

const items = ref<RadioGroupItem[]>([
  {
    label: 'System',
    description: 'This is the first option.',
    id: 'system',
  },
  {
    label: 'Light',
    description: 'This is the second option.',
    id: 'light',
  },
  {
    label: 'Dark',
    description: 'This is the third option.',
    id: 'dark',
  },
])
</script>

<template>
  <URadioGroup model-value="light" value-key="id" :items="items" />
</template>
```

### Legend

Use the `legend` prop to set the legend of the RadioGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup legend="Theme" default-value="System" :items="items" />
</template>
```

### Color

Use the `color` prop to change the color of the RadioGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup color="neutral" default-value="System" :items="items" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the RadioGroup.

```vue
<script setup lang="ts">
import type { RadioGroupItem } from '@nuxt/ui'

const items = ref<RadioGroupItem[]>([
  {
    label: 'Pro',
    value: 'pro',
    description: 'Tailored for indie hackers, freelancers and solo founders.',
  },
  {
    label: 'Startup',
    value: 'startup',
    description: 'Best suited for small teams, startups and agencies.',
  },
  {
    label: 'Enterprise',
    value: 'enterprise',
    description: 'Ideal for larger teams and organizations.',
  },
])
</script>

<template>
  <URadioGroup color="primary" variant="table" default-value="pro" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the RadioGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup size="xl" variant="list" default-value="System" :items="items" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the RadioGroup. Defaults to `vertical`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup orientation="horizontal" variant="list" default-value="System" :items="items" />
</template>
```

### Indicator

Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup indicator="end" variant="card" default-value="System" :items="items" />
</template>
```

### Disabled

Use the `disabled` prop to disable the RadioGroup.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'System',
  'Light',
  'Dark',
])
</script>

<template>
  <URadioGroup disabled default-value="System" :items="items" />
</template>
```

## API

### Props

```ts
/**
 * Props for the RadioGroup component
 */
interface RadioGroupProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  legend?: string | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value.
   * @default "\"value\" as never"
   */
  valueKey?: VK | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  items?: T | undefined;
  /**
   * The controlled value of the RadioGroup. Can be bind as `v-model`.
   */
  modelValue?: GetItemValue<T, VK, undefined, NestedItem<T>> | undefined;
  /**
   * The value of the RadioGroup when initially rendered. Use when you do not need to control the state of the RadioGroup.
   */
  defaultValue?: GetItemValue<T, VK, undefined, NestedItem<T>> | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  variant?: "card" | "list" | "table" | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The orientation the radio buttons are laid out.
   * @default "\"vertical\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * Position of the indicator.
   */
  indicator?: "start" | "end" | "hidden" | undefined;
  ui?: { root?: ClassNameValue; fieldset?: ClassNameValue; legend?: ClassNameValue; item?: ClassNameValue; container?: ClassNameValue; base?: ClassNameValue; indicator?: ClassNameValue; wrapper?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with radio items.
   */
  disabled?: boolean | undefined;
  /**
   * When `true`, keyboard navigation will loop from last item to first, and vice versa.
   */
  loop?: boolean | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the RadioGroup component
 */
interface RadioGroupSlots {
  legend(): any;
  label(): any;
  description(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the RadioGroup component
 */
interface RadioGroupEmits {
  update:modelValue: (payload: [value: GetItemValue<T, VK, undefined, NestedItem<T>>]) => void;
  change: (payload: [event: Event]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    radioGroup: {
      slots: {
        root: 'relative',
        fieldset: 'flex gap-x-2',
        legend: 'mb-1 block font-medium text-default',
        item: 'flex items-start',
        container: 'flex items-center',
        base: 'rounded-full ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2',
        indicator: 'flex items-center justify-center size-full after:bg-default after:rounded-full',
        wrapper: 'w-full',
        label: 'block font-medium text-default',
        description: 'text-muted'
      },
      variants: {
        color: {
          primary: {
            base: 'focus-visible:outline-primary',
            indicator: 'bg-primary'
          },
          secondary: {
            base: 'focus-visible:outline-secondary',
            indicator: 'bg-secondary'
          },
          success: {
            base: 'focus-visible:outline-success',
            indicator: 'bg-success'
          },
          info: {
            base: 'focus-visible:outline-info',
            indicator: 'bg-info'
          },
          warning: {
            base: 'focus-visible:outline-warning',
            indicator: 'bg-warning'
          },
          error: {
            base: 'focus-visible:outline-error',
            indicator: 'bg-error'
          },
          neutral: {
            base: 'focus-visible:outline-inverted',
            indicator: 'bg-inverted'
          }
        },
        variant: {
          list: {
            item: ''
          },
          card: {
            item: 'border border-muted rounded-lg'
          },
          table: {
            item: 'border border-muted'
          }
        },
        orientation: {
          horizontal: {
            fieldset: 'flex-row'
          },
          vertical: {
            fieldset: 'flex-col'
          }
        },
        indicator: {
          start: {
            item: 'flex-row',
            wrapper: 'ms-2'
          },
          end: {
            item: 'flex-row-reverse',
            wrapper: 'me-2'
          },
          hidden: {
            base: 'sr-only',
            wrapper: 'text-center'
          }
        },
        size: {
          xs: {
            fieldset: 'gap-y-0.5',
            legend: 'text-xs',
            base: 'size-3',
            item: 'text-xs',
            container: 'h-4',
            indicator: 'after:size-1'
          },
          sm: {
            fieldset: 'gap-y-0.5',
            legend: 'text-xs',
            base: 'size-3.5',
            item: 'text-xs',
            container: 'h-4',
            indicator: 'after:size-1'
          },
          md: {
            fieldset: 'gap-y-1',
            legend: 'text-sm',
            base: 'size-4',
            item: 'text-sm',
            container: 'h-5',
            indicator: 'after:size-1.5'
          },
          lg: {
            fieldset: 'gap-y-1',
            legend: 'text-sm',
            base: 'size-4.5',
            item: 'text-sm',
            container: 'h-5',
            indicator: 'after:size-1.5'
          },
          xl: {
            fieldset: 'gap-y-1.5',
            legend: 'text-base',
            base: 'size-5',
            item: 'text-base',
            container: 'h-6',
            indicator: 'after:size-2'
          }
        },
        disabled: {
          true: {
            item: 'opacity-75',
            base: 'cursor-not-allowed',
            label: 'cursor-not-allowed',
            description: 'cursor-not-allowed'
          }
        },
        required: {
          true: {
            legend: "after:content-['*'] after:ms-0.5 after:text-error"
          }
        }
      },
      compoundVariants: [
        {
          size: 'xs',
          variant: [
            'card',
            'table'
          ],
          class: {
            item: 'p-2.5'
          }
        },
        {
          size: 'sm',
          variant: [
            'card',
            'table'
          ],
          class: {
            item: 'p-3'
          }
        },
        {
          size: 'md',
          variant: [
            'card',
            'table'
          ],
          class: {
            item: 'p-3.5'
          }
        },
        {
          size: 'lg',
          variant: [
            'card',
            'table'
          ],
          class: {
            item: 'p-4'
          }
        },
        {
          size: 'xl',
          variant: [
            'card',
            'table'
          ],
          class: {
            item: 'p-4.5'
          }
        },
        {
          orientation: 'horizontal',
          variant: 'table',
          class: {
            item: 'first-of-type:rounded-s-lg last-of-type:rounded-e-lg',
            fieldset: 'gap-0 -space-x-px'
          }
        },
        {
          orientation: 'vertical',
          variant: 'table',
          class: {
            item: 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
            fieldset: 'gap-0 -space-y-px'
          }
        },
        {
          color: 'primary',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-secondary'
          }
        },
        {
          color: 'success',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-success'
          }
        },
        {
          color: 'info',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-info'
          }
        },
        {
          color: 'warning',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-warning'
          }
        },
        {
          color: 'error',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-error'
          }
        },
        {
          color: 'neutral',
          variant: 'card',
          class: {
            item: 'has-data-[state=checked]:border-inverted'
          }
        },
        {
          color: 'primary',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-primary/10 has-data-[state=checked]:border-primary/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'secondary',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-secondary/10 has-data-[state=checked]:border-secondary/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'success',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-success/10 has-data-[state=checked]:border-success/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'info',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-info/10 has-data-[state=checked]:border-info/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'warning',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-warning/10 has-data-[state=checked]:border-warning/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'error',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-error/10 has-data-[state=checked]:border-error/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          color: 'neutral',
          variant: 'table',
          class: {
            item: 'has-data-[state=checked]:bg-elevated has-data-[state=checked]:border-inverted/50 has-data-[state=checked]:z-[1]'
          }
        },
        {
          variant: [
            'card',
            'table'
          ],
          disabled: true,
          class: {
            item: 'cursor-not-allowed'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'list',
        orientation: 'vertical',
        indicator: 'start'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/RadioGroup.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/radio-group.ts).


# ScrollArea

## Usage

The ScrollArea component creates scrollable containers with optional virtualization for large lists.

```vue [ScrollAreaExample.vue]
<script setup lang="ts">
const heights = [320, 480, 640, 800]

// Pseudo-random height selection with longer cycle to avoid alignment patterns
function getHeight(index: number) {
  const seed = (index * 11 + 7) % 17
  return heights[seed % heights.length]!
}

const items = Array.from({ length: 1000 }).map((_, index) => {
  const height = getHeight(index)

  return {
    id: index,
    title: `Item ${index + 1}`,
    src: `https://picsum.photos/640/${height}?v=${index}`,
    width: 640,
    height
  }
})
</script>

<template>
  <UScrollArea
    v-slot="{ item, index }"
    :items="items"
    orientation="vertical"
    :virtualize="{
      gap: 16,
      lanes: 3,
      estimateSize: 480
    }"
    class="w-full h-128 p-4"
  >
    <img
      :src="item.src"
      :alt="item.title"
      :width="item.width"
      :height="item.height"
      :loading="index > 8 ? 'lazy' : 'eager'"
      class="rounded-md size-full object-cover"
    >
  </UScrollArea>
</template>
```

### Items

Use the `items` prop as an array and render each item using the default slot:

```vue [ScrollAreaItemsExample.vue]
<script setup lang="ts">
const items = Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  title: `Item ${i + 1}`,
  description: `Description for item ${i + 1}`
}))
</script>

<template>
  <UScrollArea
    v-slot="{ item, index }"
    :items="items"
    class="w-full h-96"
  >
    <UPageCard
      v-bind="item"
      :variant="index % 2 === 0 ? 'soft' : 'outline'"
      class="rounded-none"
    />
  </UScrollArea>
</template>
```

\> \[!TIP]
\> See: #with-default-slot
\> You can also use the default slot without the \`items\` prop to render custom scrollable content directly.

### Orientation

Use the `orientation` prop to change the scroll direction. Defaults to `vertical`.

```vue [ScrollAreaOrientationExample.vue]
<script setup lang="ts">
defineProps<{
  orientation?: 'vertical' | 'horizontal'
}>()

const items = Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  title: `Item ${i + 1}`,
  description: `Description for item ${i + 1}`
}))
</script>

<template>
  <UScrollArea
    v-slot="{ item, index }"
    :items="items"
    :orientation="orientation"
    class="w-full data-[orientation=vertical]:h-96"
  >
    <UPageCard
      v-bind="item"
      :variant="index % 2 === 0 ? 'soft' : 'outline'"
      class="rounded-none"
    />
  </UScrollArea>
</template>
```

### Virtualize

Use the `virtualize` prop to render only the items currently in view, significantly boosting performance when working with large datasets.

\> \[!NOTE]
\> When virtualization is enabled, customize spacing via the \`virtualize\` prop options like \`gap\`, \`paddingStart\`, and \`paddingEnd\`. Otherwise, use the \`ui\` prop to apply classes like \`gap p-4\` on the \`viewport\` slot.

\> \[!TIP]
\> If all your items have the same height, set \`skipMeasurement\` to \`true\` in the \`virtualize\` prop to skip per-item DOM measurement and rely on \`estimateSize\` instead. This significantly improves performance for large uniform lists.

```vue [ScrollAreaVirtualizeExample.vue]
<script setup lang="ts">
defineProps<{
  orientation?: 'vertical' | 'horizontal'
}>()

const items = computed(() => Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  title: `Item ${i + 1}`,
  description: `Description for item ${i + 1}`
})))
</script>

<template>
  <UScrollArea
    v-slot="{ item, index }"
    :items="items"
    :orientation="orientation"
    virtualize
    class="w-full data-[orientation=vertical]:h-96 data-[orientation=horizontal]:h-24.5"
  >
    <UPageCard
      v-bind="item"
      :variant="index % 2 === 0 ? 'soft' : 'outline'"
      class="rounded-none"
    />
  </UScrollArea>
</template>
```

## Examples

### As masonry layout

Use the `virtualize` prop with `lanes`, `gap`, and `estimateSize` options to create Pinterest-style masonry layouts with variable height items.

```vue [ScrollAreaMasonryLayoutExample.vue]
<script setup lang="ts">
withDefaults(defineProps<{
  orientation?: 'vertical' | 'horizontal'
  lanes?: number
  gap?: number
}>(), {
  orientation: 'vertical',
  lanes: 3,
  gap: 16
})

const heights = [320, 480, 640, 800]

function getHeight(index: number) {
  const seed = (index * 11 + 7) % 17
  return heights[seed % heights.length]!
}

const items = Array.from({ length: 1000 }).map((_, index) => {
  const height = getHeight(index)

  return {
    id: index,
    title: `Item ${index + 1}`,
    src: `https://picsum.photos/640/${height}?v=${index}`,
    width: 640,
    height
  }
})
</script>

<template>
  <UScrollArea
    v-slot="{ item }"
    :items="items"
    :orientation="orientation"
    :virtualize="{
      gap,
      lanes,
      estimateSize: 480
    }"
    class="w-full h-128 p-4"
  >
    <img
      :src="item.src"
      :alt="item.title"
      :width="item.width"
      :height="item.height"
      loading="lazy"
      class="rounded-md size-full object-cover"
    >
  </UScrollArea>
</template>
```

\> \[!TIP]
\> For optimal performance, set \`estimateSize\` close to your average item height. Increasing \`overscan\` improves scrolling smoothness but renders more off-screen items.

### With responsive lanes

You can use the [`useWindowSize`](https://vueuse.org/core/useWindowSize/){rel="&#x22;nofollow&#x22;"} (for viewport-based) or [`useElementSize`](https://vueuse.org/core/useElementSize/){rel="&#x22;nofollow&#x22;"} (for container-based) composables to make the `lanes` reactive.

```vue [ScrollAreaResponsiveLanesExample.vue]
<script setup lang="ts">
const items = Array.from({ length: 1000 }).map((_, index) => ({
  id: index,
  title: `Item ${index + 1}`,
  src: `https://picsum.photos/640/480?v=${index}`,
  width: 640,
  height: 480
}))

const gap = 16
const scrollArea = useTemplateRef('scrollArea')
const { width } = useElementSize(() => scrollArea.value?.$el)

const lanes = computed(() => Math.max(1, Math.min(4, Math.floor(width.value / 200))))
const laneWidth = computed(() => (width.value - (lanes.value - 1) * gap) / lanes.value)
const estimateSize = computed(() => laneWidth.value * (480 / 640))
</script>

<template>
  <UScrollArea
    ref="scrollArea"
    v-slot="{ item }"
    :items="items"
    :virtualize="{
      gap,
      lanes,
      estimateSize,
      skipMeasurement: true
    }"
    class="w-full h-96 p-4"
  >
    <img
      :src="item.src"
      :alt="item.title"
      :width="item.width"
      :height="item.height"
      loading="lazy"
      class="rounded-md size-full object-cover"
    >
  </UScrollArea>
</template>
```

### With programmatic scroll

You can use the exposed `virtualizer` to programmatically control scroll position.

```vue [ScrollAreaScrollToExample.vue]
<script setup lang="ts">
const items = computed(() => Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  title: `Item ${i + 1}`
})))

const scrollArea = useTemplateRef('scrollArea')

const targetIndex = ref(500)

function scrollToTop() {
  scrollArea.value?.virtualizer?.scrollToIndex(0, { align: 'start', behavior: 'smooth' })
}

function scrollToBottom() {
  scrollArea.value?.virtualizer?.scrollToIndex(items.value.length - 1, { align: 'end', behavior: 'smooth' })
}

function scrollToItem(index: number) {
  scrollArea.value?.virtualizer?.scrollToIndex(index - 1, { align: 'center', behavior: 'smooth' })
}
</script>

<template>
  <div class="w-full">
    <UScrollArea
      v-slot="{ item, index }"
      ref="scrollArea"
      :items="items"
      :virtualize="{
        estimateSize: 72,
        skipMeasurement: true
      }"
      class="h-96 w-full"
    >
      <UPageCard
        v-bind="item"
        :variant="index % 2 === 0 ? 'soft' : 'outline'"
        class="rounded-none isolate"
        :class="[index === (targetIndex - 1) && 'bg-primary']"
      />
    </UScrollArea>

    <UFieldGroup size="sm" class="px-4 py-3 border-t border-muted w-full">
      <UButton icon="i-lucide-arrow-up-to-line" color="neutral" variant="outline" @click="scrollToTop">
        Top
      </UButton>
      <UButton icon="i-lucide-arrow-down-to-line" color="neutral" variant="outline" @click="scrollToBottom">
        Bottom
      </UButton>
      <UButton icon="i-lucide-navigation" color="neutral" variant="outline" @click="scrollToItem(targetIndex || 500)">
        Go to {{ targetIndex || 500 }}
      </UButton>
    </UFieldGroup>
  </div>
</template>
```

### With infinite scroll

You can use the [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/){rel="&#x22;nofollow&#x22;"} composable to load more data as the user scrolls.

```vue [ScrollAreaInfiniteScrollExample.vue]
<script setup lang="ts">
import { useInfiniteScroll } from '@vueuse/core'

type User = {
  id: number
  firstName: string
  lastName: string
  username: string
  email: string
  image: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status } = useLazyFetch('https://dummyjson.com/users?limit=10&select=firstName,lastName,username,email,image', {
  key: 'scroll-area-users-infinite-scroll',
  params: { skip },
  transform: (data?: UserResponse) => {
    return data?.users
  },
  server: false
})

const users = ref<User[]>([])

watch(data, () => {
  users.value = [
    ...users.value,
    ...(data.value || [])
  ]
})

const scrollArea = useTemplateRef('scrollArea')

onMounted(() => {
  useInfiniteScroll(scrollArea.value?.$el, () => {
    skip.value += 10
  }, {
    distance: 200,
    canLoadMore: () => {
      return status.value !== 'pending'
    }
  })
})
</script>

<template>
  <UScrollArea
    ref="scrollArea"
    v-slot="{ item }"
    :items="users"
    :virtualize="{
      estimateSize: 88,
      skipMeasurement: true
    }"
    class="h-96 w-full"
  >
    <UPageCard
      orientation="horizontal"
      class="rounded-none"
    >
      <UUser
        :name="`${item.firstName} ${item.lastName}`"
        :description="item.email"
        :avatar="{ src: item.image, alt: item.firstName, loading: 'lazy' as const }"
        size="lg"
      />
    </UPageCard>
  </UScrollArea>

  <UProgress
    v-if="status === 'pending' || status === 'idle'"
    indeterminate
    size="xs"
    class="absolute top-0 inset-x-0 z-1"
    :ui="{ base: 'bg-default' }"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`server: false\` to fetch data on the client without blocking the initial render. The loading state checks for both \`pending\` and \`idle\` status to display a loading indicator before and during the fetch. Additional pages are loaded as the user scrolls.

### With default slot

You can use the default slot without the `items` prop to render custom scrollable content directly.

```vue [ScrollAreaDefaultSlotExample.vue]
<template>
  <UScrollArea class="h-96 w-full" :ui="{ viewport: 'gap-4 p-4' }">
    <UPageCard title="Section 1" description="Custom content without using the items prop." />
    <UPageCard title="Section 2" description="Custom content without using the items prop." />
    <UPageCard title="Section 3" description="Custom content without using the items prop." />
    <UPageCard title="Section 4" description="Custom content without using the items prop." />
    <UPageCard title="Section 5" description="Custom content without using the items prop." />
    <UPageCard title="Section 6" description="Custom content without using the items prop." />
  </UScrollArea>
</template>
```

## API

### Props

```ts
/**
 * Props for the ScrollArea component
 */
interface ScrollAreaProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * The scroll direction.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Array of items to render.
   */
  items?: T[] | undefined;
  /**
   * Enable virtualization for large lists.
   * @default "false"
   */
  virtualize?: boolean | ScrollAreaVirtualizeOptions | undefined;
  ui?: { root?: ClassNameValue; viewport?: ClassNameValue; item?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the ScrollArea component
 */
interface ScrollAreaSlots {
  default(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the ScrollArea component
 */
interface ScrollAreaEmits {
  scroll: (payload: [isScrolling: boolean]) => void;
}
```

### Expose

You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
const scrollArea = useTemplateRef('scrollArea')

// Scroll to a specific item
function scrollToItem(index: number) {
  scrollArea.value?.virtualizer?.scrollToIndex(index, { align: 'center' })
}
</script>

<template>
  <UScrollArea ref="scrollArea" :items="items" virtualize />
</template>
```

This will give you access to the following:

| Name                                                                                                                              | Type                                                                                                                                               | Description                                                                                                                                                                    |
| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `$el`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}         | `HTMLElement`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                  | The root element of the component.                                                                                                                                             |
| `virtualizer`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<Virtualizer> | undefined`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | The [TanStack Virtual](https://tanstack.com/virtual/latest/docs/api/virtualizer){rel="&#x22;nofollow&#x22;"} virtualizer instance (`undefined` if virtualization is disabled). |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    scrollArea: {
      slots: {
        root: 'relative',
        viewport: 'relative flex',
        item: ''
      },
      variants: {
        orientation: {
          vertical: {
            root: 'overflow-y-auto overflow-x-hidden',
            viewport: 'flex-col',
            item: ''
          },
          horizontal: {
            root: 'overflow-x-auto overflow-y-hidden',
            viewport: 'flex-row',
            item: ''
          }
        }
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/ScrollArea.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/scroll-area.ts).


# Select

## Usage

Use the `v-model` directive to control the value of the Select or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" :items="items" />
</template>
```

### Items

Use the `items` prop as an array of strings, numbers or booleans:

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" class="w-48" :items="items" />
</template>
```

You can also pass an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`value?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#value-key)
- [`type?: "label" | "separator" | "item"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-items-type)
- [`icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-icons-in-items)
- [`avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-avatar-in-items)
- [`chip?: ChipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-chip-in-items)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'

const items = ref<SelectItem[]>([
  {
    label: 'Backlog',
    value: 'backlog',
  },
  {
    label: 'Todo',
    value: 'todo',
  },
  {
    label: 'In Progress',
    value: 'in_progress',
  },
  {
    label: 'Done',
    value: 'done',
  },
])
</script>

<template>
  <USelect model-value="backlog" class="w-48" :items="items" />
</template>
```

\> \[!CAUTION]
\> When using objects, you need to reference the \`value\` property of the object in the \`v-model\` directive or the \`default-value\` prop.

You can also pass an array of arrays to the `items` prop to display separated groups of items.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  [
    'Apple',
    'Banana',
    'Blueberry',
    'Grapes',
    'Pineapple',
  ],
  [
    'Aubergine',
    'Broccoli',
    'Carrot',
    'Courgette',
    'Leek',
  ],
])
</script>

<template>
  <USelect model-value="Apple" class="w-48" :items="items" />
</template>
```

### Value Key

You can change the property that is used to set the value by using the `value-key` prop. Defaults to `value`.

```vue
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'

const items = ref<SelectItem[]>([
  {
    label: 'Backlog',
    id: 'backlog',
  },
  {
    label: 'Todo',
    id: 'todo',
  },
  {
    label: 'In Progress',
    id: 'in_progress',
  },
  {
    label: 'Done',
    id: 'done',
  },
])
</script>

<template>
  <USelect model-value="backlog" value-key="id" class="w-48" :items="items" />
</template>
```

\> \[!TIP]
\> Use the \`by\` prop to compare objects by a field instead of reference when the \`model-value\` is an object.

### Multiple

Use the `multiple` prop to allow multiple selections, the selected items will be separated by a comma in the trigger.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect multiple class="w-48" :items="items" />
</template>
```

\> \[!CAUTION]
\> Ensure to pass an array to the \`default-value\` prop or the \`v-model\` directive.

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect placeholder="Select status" class="w-48" :items="items" />
</template>
```

### Content

Use the `content` prop to control how the Select content is rendered, like its `align` or `side` for example.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" class="w-48" :items="items" />
</template>
```

### Arrow

Use the `arrow` prop to display an arrow on the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" arrow class="w-48" :items="items" />
</template>
```

### Color

Use the `color` prop to change the ring color when the Select is focused.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" color="neutral" highlight class="w-48" :items="items" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" color="neutral" variant="subtle" :highlight="false" class="w-48" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" size="xl" class="w-48" :items="items" />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" icon="i-lucide-search" size="md" class="w-48" :items="items" />
</template>
```

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-chevron-down`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" trailing-icon="i-lucide-arrow-down" size="md" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Selected Icon

Use the `selected-icon` prop to customize the icon when an item is selected. Defaults to `i-lucide-check`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" selected-icon="i-lucide-flame" size="md" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.check\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.check\` key.

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Nuxt',
  'NuxtHub',
  'NuxtLabs',
  'Nuxt Modules',
  'Nuxt Community',
])
</script>

<template>
  <USelect model-value="Nuxt" class="w-48" :items="items" />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" loading :trailing="false" class="w-48" :items="items" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect model-value="Backlog" loading loading-icon="i-lucide-loader" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the Select.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelect disabled placeholder="Select status" class="w-48" :items="items" />
</template>
```

## Examples

### With items type

You can use the `type` property with `separator` to display a separator between items or `label` to display a label.

```vue
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'

const items = ref<SelectItem[]>([
  {
    type: 'label',
    label: 'Fruits',
  },
  'Apple',
  'Banana',
  'Blueberry',
  'Grapes',
  'Pineapple',
  {
    type: 'separator',
  },
  {
    type: 'label',
    label: 'Vegetables',
  },
  'Aubergine',
  'Broccoli',
  'Carrot',
  'Courgette',
  'Leek',
])
</script>

<template>
  <USelect model-value="Apple" class="w-48" :items="items" />
</template>
```

### With icon in items

You can use the `icon` property to display an [Icon](https://ui.nuxt.com/docs/components/icon) inside the items.

```vue [SelectItemsIconExample.vue]
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'Backlog',
    value: 'backlog',
    icon: 'i-lucide-circle-help'
  },
  {
    label: 'Todo',
    value: 'todo',
    icon: 'i-lucide-circle-plus'
  },
  {
    label: 'In Progress',
    value: 'in_progress',
    icon: 'i-lucide-circle-arrow-up'
  },
  {
    label: 'Done',
    value: 'done',
    icon: 'i-lucide-circle-check'
  }
] satisfies SelectItem[])

const value = ref(items.value[0]?.value)

const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
</script>

<template>
  <USelect v-model="value" :items="items" value-key="value" :icon="icon" class="w-48" />
</template>
```

\> \[!NOTE]
\> In this example, the icon is computed from the \`value\` property of the selected item.

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected icon.

### With avatar in items

You can use the `avatar` property to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the items.

```vue [SelectItemsAvatarExample.vue]
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'benjamincanac',
    value: 'benjamincanac',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  },
  {
    label: 'romhml',
    value: 'romhml',
    avatar: {
      src: 'https://github.com/romhml.png',
      alt: 'romhml',
      loading: 'lazy' as const
    }
  },
  {
    label: 'noook',
    value: 'noook',
    avatar: {
      src: 'https://github.com/noook.png',
      alt: 'noook',
      loading: 'lazy' as const
    }
  },
  {
    label: 'sandros94',
    value: 'sandros94',
    avatar: {
      src: 'https://github.com/sandros94.png',
      alt: 'sandros94',
      loading: 'lazy' as const
    }
  }
] satisfies SelectItem[])

const value = ref(items.value[0]?.value)

const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
</script>

<template>
  <USelect v-model="value" :items="items" value-key="value" :avatar="avatar" class="w-48" />
</template>
```

\> \[!NOTE]
\> In this example, the avatar is computed from the \`value\` property of the selected item.

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected avatar.

### With chip in items

You can use the `chip` property to display a [Chip](https://ui.nuxt.com/docs/components/chip) inside the items.

```vue [SelectItemsChipExample.vue]
<script setup lang="ts">
import type { SelectItem, ChipProps } from '@nuxt/ui'

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error'
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success'
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info'
    }
  }
] satisfies SelectItem[])

const value = ref(items.value[0]?.value)

function getChip(value: string) {
  return items.value.find(item => item.value === value)?.chip
}
</script>

<template>
  <USelect v-model="value" :items="items" value-key="value" class="w-48">
    <template #leading="{ modelValue, ui }">
      <UChip
        v-if="modelValue"
        v-bind="getChip(modelValue)"
        inset
        standalone
        :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
        :class="ui.itemLeadingChip()"
      />
    </template>
  </USelect>
</template>
```

\> \[!NOTE]
\> In this example, the \`#leading\` slot is used to display the selected chip.

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [SelectOpenExample.vue]
<script setup lang="ts">
const open = ref(false)
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <USelect v-model="value" v-model:open="open" :items="items" class="w-48" />
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Select by pressing .

### With rotating icon

Here is an example with a rotating icon that indicates the open state of the Select.

```vue [SelectIconExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')
</script>

<template>
  <USelect
    v-model="value"
    :items="items"
    :ui="{
      trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
    }"
    class="w-48"
  />
</template>
```

### With fetched items

You can fetch items from an API and use them in the Select.

```vue [SelectFetchExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users',
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function getUserAvatar(value: string) {
  return users.value?.find(user => user.value === value)?.avatar || {}
}

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelect
    :items="users"
    :loading="status === 'pending'"
    icon="i-lucide-user"
    placeholder="Select user"
    value-key="value"
    class="w-48"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="getUserAvatar(modelValue)"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>
  </USelect>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the menu opens, avoiding unnecessary API calls on page load.

### With infinite scroll `4.4+`

You can use the [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/){rel="&#x22;nofollow&#x22;"} composable to load more data as the user scrolls.

```vue [SelectInfiniteScrollExample.vue]
<script setup lang="ts">
import { useInfiniteScroll } from '@vueuse/core'

type User = {
  firstName: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status, execute } = await useLazyFetch('https://dummyjson.com/users?limit=10&select=firstName', {
  key: 'select-users-infinite-scroll',
  params: { skip },
  transform: (data?: UserResponse) => {
    return data?.users.map(user => user.firstName)
  },
  immediate: false
})

const users = ref<string[]>([])

watch(data, () => {
  users.value = [
    ...users.value,
    ...(data.value || [])
  ]
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}

const select = useTemplateRef('select')

onMounted(() => {
  useInfiniteScroll(() => select.value?.viewportRef, () => {
    skip.value += 10
  }, {
    canLoadMore: () => {
      return status.value !== 'pending'
    }
  })
})
</script>

<template>
  <USelect
    ref="select"
    placeholder="Select user"
    :items="users"
    @update:open="onOpen"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` so data is only loaded as the user scrolls.

### With full content width

You can expand the content to the full width of its items by adding the `min-w-fit` class on the `ui.content` slot.

```vue [SelectContentWidthExample.vue]
<script setup lang="ts">
const value = ref<string>()

const { data: users, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users-email',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      email: user.email,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelect
    v-model="value"
    :items="users"
    placeholder="Select user"
    value-key="value"
    :ui="{ content: 'min-w-fit' }"
    class="w-48"
    @update:open="onOpen"
  >
    <template #item-label="{ item }">
      {{ item.label }}

      <span class="text-muted">
        {{ item.email }}
      </span>
    </template>
  </USelect>
</template>
```

\> \[!TIP]
\> You can also change the content width globally in your \`app.config.ts\`:
\> \`\`\`text
\> export default defineAppConfig({
\> ui: {
\> select: {
\> slots: {
\> content: 'min-w-fit'
\> }
\> }
\> }
\> })
\>
\> \`\`\`

## API

### Props

```ts
/**
 * Props for the Select component
 */
interface SelectProps {
  id?: string | undefined;
  /**
   * The placeholder text when the select is empty.
   */
  placeholder?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<SelectContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<SelectContentImplEmits>>) | undefined;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<SelectArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the menu in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value.
   * @default "\"value\" as never"
   */
  valueKey?: VK | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  items?: T | undefined;
  /**
   * The value of the Select when initially rendered. Use when you do not need to control the state of the Select.
   */
  defaultValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | undefined;
  /**
   * The controlled value of the Select. Can be bind as `v-model`.
   */
  modelValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | undefined;
  modelModifiers?: Mod | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: M | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; value?: ClassNameValue; placeholder?: ClassNameValue; arrow?: ClassNameValue; content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with Select
   */
  disabled?: boolean | undefined;
  /**
   * The controlled open state of the Select. Can be bind as `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the select when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * Native html input `autocomplete` attribute.
   */
  autocomplete?: string | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Select component
 */
interface SelectSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  content-top(): any;
  content-bottom(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Select component
 */
interface SelectEmits {
  update:modelValue: (payload: [value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod>]) => void;
  update:open: (payload: [value: boolean]) => void;
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                                |
| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggerRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}  | `Ref<HTMLButtonElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `viewportRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLDivElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    select: {
      slots: {
        base: [
          'relative group rounded-md inline-flex items-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed',
        value: 'truncate pointer-events-none',
        placeholder: 'truncate text-dimmed',
        arrow: 'fill-bg stroke-default',
        content: 'max-h-60 w-(--reka-select-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-select-content-transform-origin) pointer-events-auto flex flex-col',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        empty: 'text-center text-muted',
        label: 'font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: [
          'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
          'transition-colors before:transition-colors'
        ],
        itemLeadingIcon: [
          'shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
          'transition-colors'
        ],
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemLeadingChip: 'shrink-0',
        itemLeadingChipSize: '',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0',
        itemWrapper: 'flex-1 flex flex-col min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted'
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-xs gap-1',
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            empty: 'p-2 text-xs'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-xs gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            empty: 'p-2.5 text-xs'
          },
          md: {
            base: 'px-2.5 py-1.5 text-sm gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            empty: 'p-2.5 text-sm'
          },
          lg: {
            base: 'px-3 py-2 text-sm gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            empty: 'p-3 text-sm'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemLeadingChip: 'size-6',
            itemLeadingChipSize: 'lg',
            itemTrailingIcon: 'size-6',
            empty: 'p-3 text-base'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented hover:bg-elevated disabled:bg-default',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented hover:bg-accented/75 disabled:bg-elevated',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus:ring-2 focus:ring-inset focus:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Select.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/select.ts).


# SelectMenu

## Usage

Use the `v-model` directive to control the value of the SelectMenu or the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" :items="items" />
</template>
```

\> \[!TIP]
\> Use this over a \[\`Select\`]\(/docs/components/select) to take advantage of Reka UI's \[\`Combobox\`]\(https\://reka-ui.com/docs/components/combobox) component that offers search capabilities and multiple selection.

\> \[!NOTE]
\> This component is similar to the \[\`InputMenu\`]\(/docs/components/input-menu) but it's using a Select instead of an Input with the search inside the menu.

### Items

Use the `items` prop as an array of strings, numbers or booleans:

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" class="w-48" :items="items" />
</template>
```

You can also pass an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`type?: "label" | "separator" | "item"`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-items-type)
- [`icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-icons-in-items)
- [`avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-avatar-in-items)
- [`chip?: ChipProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-chip-in-items)
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `onSelect?: (e: Event) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref<SelectMenuItem[]>([
  {
    label: 'Backlog',
  },
  {
    label: 'Todo',
  },
  {
    label: 'In Progress',
  },
  {
    label: 'Done',
  },
])
</script>

<template>
  <USelectMenu class="w-48" :items="items" />
</template>
```

\> \[!CAUTION]
\> Unlike the \[\`Select\`]\(/docs/components/select) component, the SelectMenu expects the whole object to be passed to the \`v-model\` directive or the \`default-value\` prop by default.

You can also pass an array of arrays to the `items` prop to display separated groups of items.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  [
    'Apple',
    'Banana',
    'Blueberry',
    'Grapes',
    'Pineapple',
  ],
  [
    'Aubergine',
    'Broccoli',
    'Carrot',
    'Courgette',
    'Leek',
  ],
])
</script>

<template>
  <USelectMenu model-value="Apple" class="w-48" :items="items" />
</template>
```

### Value Key

You can choose to bind a single property of the object rather than the whole object by using the `value-key` prop. Defaults to `undefined`.

```vue
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref<SelectMenuItem[]>([
  {
    label: 'Backlog',
    id: 'backlog',
  },
  {
    label: 'Todo',
    id: 'todo',
  },
  {
    label: 'In Progress',
    id: 'in_progress',
  },
  {
    label: 'Done',
    id: 'done',
  },
])
</script>

<template>
  <USelectMenu model-value="todo" value-key="id" class="w-48" :items="items" />
</template>
```

\> \[!TIP]
\> Use the \`by\` prop to compare objects by a field instead of reference when the \`model-value\` is an object.

### Multiple

Use the `multiple` prop to allow multiple selections, the selected items will be separated by a comma in the trigger.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu multiple class="w-48" :items="items" />
</template>
```

\> \[!CAUTION]
\> Ensure to pass an array to the \`default-value\` prop or the \`v-model\` directive.

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu placeholder="Select status" class="w-48" :items="items" />
</template>
```

### Search Input

Use the `search-input` prop to customize or hide the search input (with `false` value).

You can pass any property from the [Input](https://ui.nuxt.com/docs/components/input) component to customize it.

```vue
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref<SelectMenuItem[]>([
  {
    label: 'Backlog',
    icon: 'i-lucide-circle-help',
  },
  {
    label: 'Todo',
    icon: 'i-lucide-circle-plus',
  },
  {
    label: 'In Progress',
    icon: 'i-lucide-circle-arrow-up',
  },
  {
    label: 'Done',
    icon: 'i-lucide-circle-check',
  },
])
</script>

<template>
  <USelectMenu class="w-48" :items="items" />
</template>
```

\> \[!TIP]
\> You can set the \`search-input\` prop to \`false\` to hide the search input.

### Content

Use the `content` prop to control how the SelectMenu content is rendered, like its `align` or `side` for example.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" class="w-48" :items="items" />
</template>
```

### Arrow

Use the `arrow` prop to display an arrow on the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" arrow class="w-48" :items="items" />
</template>
```

### Color

Use the `color` prop to change the ring color when the SelectMenu is focused.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" color="neutral" highlight class="w-48" :items="items" />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" color="neutral" variant="subtle" :highlight="false" class="w-48" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" size="xl" class="w-48" :items="items" />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" icon="i-lucide-search" size="md" class="w-48" :items="items" />
</template>
```

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-chevron-down`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" trailing-icon="i-lucide-arrow-down" size="md" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Selected Icon

Use the `selected-icon` prop to customize the icon when an item is selected. Defaults to `i-lucide-check`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" selected-icon="i-lucide-flame" size="md" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.check\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.check\` key.

### Clear `4.4+`

Use the `clear` prop to display a clear button when a value is selected.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" clear class="w-48" :items="items" />
</template>
```

### Clear Icon `4.4+`

Use the `clear-icon` prop to customize the clear button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" clear clear-icon="i-lucide-trash" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Avatar

Use the `avatar` prop to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Nuxt',
  'NuxtHub',
  'NuxtLabs',
  'Nuxt Modules',
  'Nuxt Community',
])
</script>

<template>
  <USelectMenu model-value="Nuxt" class="w-48" :items="items" />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" loading :trailing="false" class="w-48" :items="items" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu model-value="Backlog" loading loading-icon="i-lucide-loader" class="w-48" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the SelectMenu.

```vue
<script setup lang="ts">
const items = ref<undefined>([
  'Backlog',
  'Todo',
  'In Progress',
  'Done',
])
</script>

<template>
  <USelectMenu disabled placeholder="Select status" class="w-48" :items="items" />
</template>
```

## Examples

### With items type

You can use the `type` property with `separator` to display a separator between items or `label` to display a label.

```vue
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref<SelectMenuItem[]>([
  {
    type: 'label',
    label: 'Fruits',
  },
  'Apple',
  'Banana',
  'Blueberry',
  'Grapes',
  'Pineapple',
  {
    type: 'separator',
  },
  {
    type: 'label',
    label: 'Vegetables',
  },
  'Aubergine',
  'Broccoli',
  'Carrot',
  'Courgette',
  'Leek',
])
</script>

<template>
  <USelectMenu model-value="Apple" class="w-48" :items="items" />
</template>
```

### With icon in items

You can use the `icon` property to display an [Icon](https://ui.nuxt.com/docs/components/icon) inside the items.

```vue [SelectMenuItemsIconExample.vue]
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'Backlog',
    value: 'backlog',
    icon: 'i-lucide-circle-help'
  },
  {
    label: 'Todo',
    value: 'todo',
    icon: 'i-lucide-circle-plus'
  },
  {
    label: 'In Progress',
    value: 'in_progress',
    icon: 'i-lucide-circle-arrow-up'
  },
  {
    label: 'Done',
    value: 'done',
    icon: 'i-lucide-circle-check'
  }
] satisfies SelectMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <USelectMenu v-model="value" :icon="value?.icon" :items="items" class="w-48" />
</template>
```

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected icon.

### With avatar in items

You can use the `avatar` property to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the items.

```vue [SelectMenuItemsAvatarExample.vue]
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items = ref([
  {
    label: 'benjamincanac',
    value: 'benjamincanac',
    avatar: {
      src: 'https://github.com/benjamincanac.png',
      alt: 'benjamincanac',
      loading: 'lazy' as const
    }
  },
  {
    label: 'romhml',
    value: 'romhml',
    avatar: {
      src: 'https://github.com/romhml.png',
      alt: 'romhml',
      loading: 'lazy' as const
    }
  },
  {
    label: 'noook',
    value: 'noook',
    avatar: {
      src: 'https://github.com/noook.png',
      alt: 'noook',
      loading: 'lazy' as const
    }
  },
  {
    label: 'sandros94',
    value: 'sandros94',
    avatar: {
      src: 'https://github.com/sandros94.png',
      alt: 'sandros94',
      loading: 'lazy' as const
    }
  }
] satisfies SelectMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <USelectMenu v-model="value" :avatar="value?.avatar" :items="items" class="w-48" />
</template>
```

\> \[!TIP]
\> You can also use the \`#leading\` slot to display the selected avatar.

### With chip in items

You can use the `chip` property to display a [Chip](https://ui.nuxt.com/docs/components/chip) inside the items.

```vue [SelectMenuItemsChipExample.vue]
<script setup lang="ts">
import type { SelectMenuItem, ChipProps } from '@nuxt/ui'

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error'
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success'
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info'
    }
  }
] satisfies SelectMenuItem[])

const value = ref(items.value[0])
</script>

<template>
  <USelectMenu v-model="value" :items="items" class="w-48">
    <template #leading="{ modelValue, ui }">
      <UChip
        v-if="modelValue"
        v-bind="modelValue.chip"
        inset
        standalone
        :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
        :class="ui.itemLeadingChip()"
      />
    </template>
  </USelectMenu>
</template>
```

\> \[!NOTE]
\> In this example, the \`#leading\` slot is used to display the selected chip.

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [SelectMenuOpenExample.vue]
<script setup lang="ts">
const open = ref(false)
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <USelectMenu v-model="value" v-model:open="open" :items="items" class="w-48" />
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the SelectMenu by pressing .

### Control search term

Use the `v-model:search-term` directive to control the search term.

```vue [SelectMenuSearchTermExample.vue]
<script setup lang="ts">
const searchTerm = ref('D')
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')
</script>

<template>
  <USelectMenu v-model="value" v-model:search-term="searchTerm" :items="items" class="w-48" />
</template>
```

### With rotating icon

Here is an example with a rotating icon that indicates the open state of the SelectMenu.

```vue [SelectMenuIconExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')
</script>

<template>
  <USelectMenu
    v-model="value"
    :items="items"
    :ui="{
      trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
    }"
    class="w-48"
  />
</template>
```

### With create item

Use the `create-item` prop to enable users to add custom values that aren't in the predefined options.

```vue [SelectMenuCreateItemExample.vue]
<script setup lang="ts">
const items = ref(['Backlog', 'Todo', 'In Progress', 'Done'])
const value = ref('Backlog')

function onCreate(item: string) {
  items.value.push(item)

  value.value = item
}
</script>

<template>
  <USelectMenu
    v-model="value"
    create-item
    :items="items"
    class="w-48"
    @create="onCreate"
  />
</template>
```

\> \[!NOTE]
\> The create option shows when no match is found by default. Set it to \`always\` to show it even when similar values exist.

\> \[!TIP]
\> See: #emits
\> Use the \`@create\` event to handle the creation of the item. You will receive the event and the item as arguments.

### With fetched items

You can fetch items from an API and use them in the SelectMenu.

```vue [SelectMenuFetchExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users',
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelectMenu
    :items="users"
    :loading="status === 'pending'"
    icon="i-lucide-user"
    placeholder="Select user"
    class="w-48"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>
  </USelectMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the menu opens, avoiding unnecessary API calls on page load.

### With ignore filter

Set the `ignore-filter` prop to `true` to disable the internal search and use your own search logic.

```vue [SelectMenuIgnoreFilterExample.vue]
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'
import type { AvatarProps } from '@nuxt/ui'

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'select-menu-users-search',
  params: { q: searchTermDebounced },
  transform: (data: { id: number, name: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelectMenu
    v-model:search-term="searchTerm"
    :items="users"
    :search-input="{
      icon: 'i-lucide-search',
      loading: status === 'pending'
    }"
    ignore-filter
    icon="i-lucide-user"
    placeholder="Select user"
    class="w-48"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>
  </USelectMenu>
</template>
```

\> \[!NOTE]
\> This example uses \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/#refdebounced) to debounce the API calls. The fetch is deferred with \`immediate: false\` so no request is made until the menu opens.

### With filter fields

Use the `filter-fields` prop with an array of fields to filter on. Defaults to `[labelKey]`.

```vue [SelectMenuFilterFieldsExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users-email',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      email: user.email,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelectMenu
    :items="users"
    :loading="status === 'pending'"
    :filter-fields="['label', 'email']"
    icon="i-lucide-user"
    placeholder="Select user"
    class="w-80"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <UAvatar
        v-if="modelValue"
        v-bind="modelValue.avatar"
        :size="(ui.leadingAvatarSize() as AvatarProps['size'])"
        :class="ui.leadingAvatar()"
      />
    </template>

    <template #item-label="{ item }">
      {{ item.label }}

      <span class="text-muted">
        {{ item.email }}
      </span>
    </template>
  </USelectMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only fetch data when the menu opens, avoiding unnecessary API calls on page load.

### With virtualization `4.1+`

Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.

\> \[!WARNING]
\> See: https\://github.com/unovue/reka-ui/issues/1885
\> When enabled, all groups are flattened into a single list due to a limitation of Reka UI.

```vue [SelectMenuVirtualizeExample.vue]
<script setup lang="ts">
import type { SelectMenuItem } from '@nuxt/ui'

const items: SelectMenuItem[] = Array(1000).fill(0).map((_, i) => ({
  label: `item-${i}`,
  value: i
}))
</script>

<template>
  <USelectMenu virtualize :items="items" class="w-48" />
</template>
```

### With infinite scroll `4.4+`

You can use the [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/){rel="&#x22;nofollow&#x22;"} composable to load more data as the user scrolls.

```vue [SelectMenuInfiniteScrollExample.vue]
<script setup lang="ts">
import { useInfiniteScroll } from '@vueuse/core'

type User = {
  firstName: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status, execute } = await useLazyFetch('https://dummyjson.com/users?limit=10&select=firstName', {
  key: 'select-menu-users-infinite-scroll',
  params: { skip },
  transform: (data?: UserResponse) => {
    return data?.users.map(user => user.firstName)
  },
  immediate: false
})

const users = ref<string[]>([])

watch(data, () => {
  users.value = [
    ...users.value,
    ...(data.value || [])
  ]
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}

const selectMenu = useTemplateRef('selectMenu')

onMounted(() => {
  useInfiniteScroll(() => selectMenu.value?.viewportRef, () => {
    skip.value += 10
  }, {
    canLoadMore: () => {
      return status.value !== 'pending'
    }
  })
})
</script>

<template>
  <USelectMenu
    ref="selectMenu"
    placeholder="Select user"
    :items="users"
    @update:open="onOpen"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` so data is only loaded as the user scrolls.

### With full content width

You can expand the content to the full width of its items by adding the `min-w-fit` class on the `ui.content` slot.

```vue [SelectMenuContentWidthExample.vue]
<script setup lang="ts">
const { data: users, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'typicode-users-email',
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({
      label: user.name,
      email: user.email,
      value: String(user.id),
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
    }))
  },
  immediate: false
})

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelectMenu
    :items="users"
    icon="i-lucide-user"
    placeholder="Select user"
    :ui="{ content: 'min-w-fit' }"
    class="w-48"
    @update:open="onOpen"
  >
    <template #item-label="{ item }">
      {{ item.label }}

      <span class="text-muted">
        {{ item.email }}
      </span>
    </template>
  </USelectMenu>
</template>
```

\> \[!TIP]
\> You can also change the content width globally in your \`app.config.ts\`:
\> \`\`\`text
\> export default defineAppConfig({
\> ui: {
\> selectMenu: {
\> slots: {
\> content: 'min-w-fit'
\> }
\> }
\> }
\> })
\>
\> \`\`\`

### As a country picker

You can use the SelectMenu as a country picker with lazy loading. Countries are only fetched when the menu is first opened.

```vue [SelectMenuCountriesExample.vue]
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
  name: string
  code: string
  emoji: string
}[]>('/api/countries.json', {
  key: 'api-countries',
  immediate: false
})

function onOpen() {
  if (!countries.value?.length) {
    execute()
  }
}
</script>

<template>
  <USelectMenu
    :items="countries"
    :loading="status === 'pending'"
    label-key="name"
    :search-input="{ icon: 'i-lucide-search' }"
    placeholder="Select country"
    class="w-48"
    @update:open="onOpen"
  >
    <template #leading="{ modelValue, ui }">
      <span v-if="modelValue" class="size-5 text-center">
        {{ modelValue?.emoji }}
      </span>
      <UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
    </template>
    <template #item-leading="{ item }">
      <span class="size-5 text-center">
        {{ item.emoji }}
      </span>
    </template>
  </USelectMenu>
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`immediate: false\` to only load countries when the menu is first opened.

## API

### Props

```ts
/**
 * Props for the SelectMenu component
 */
interface SelectMenuProps {
  id?: string | undefined;
  /**
   * The placeholder text when the select is empty.
   */
  placeholder?: string | undefined;
  /**
   * Whether to display the search input or not.
   * Can be an object to pass additional props to the input.
   * `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
   * @default "true"
   */
  searchInput?: boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "sm" | "md" | "xs" | "lg" | "xl" | undefined;
  required?: boolean | undefined;
  /**
   * The icon displayed to open the menu.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when an item is selected.
   */
  selectedIcon?: any;
  /**
   * Display a clear button to reset the model value.
   * Can be an object to pass additional props to the Button.
   */
  clear?: (C & boolean) | (C & Partial<Omit<ButtonProps, LinkPropsKeys>>) | undefined;
  /**
   * The icon displayed in the clear button.
   */
  clearIcon?: any;
  /**
   * The content of the menu.
   */
  content?: (Omit<ComboboxContentProps, "asChild" | "as" | "forceMount"> & Partial<EmitsToProps<DismissableLayerEmits>>) | undefined;
  /**
   * Display an arrow alongside the menu.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<ComboboxArrowProps, "asChild" | "as"> | undefined;
  /**
   * Render the menu in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, all groups are flattened into a single list due to a limitation of Reka UI (https://github.com/unovue/reka-ui/issues/1885).
   * @default "false"
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the value instead of the object itself.
   */
  valueKey?: VK | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the label.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * When `items` is an array of objects, select the field to use as the description.
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  items?: T | undefined;
  /**
   * The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu.
   */
  defaultValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C> | undefined;
  /**
   * The controlled value of the SelectMenu. Can be binded-with with `v-model`.
   */
  modelValue?: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C> | undefined;
  modelModifiers?: Mod | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: M | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Determines if custom user input that does not exist in options can be added.
   */
  createItem?: boolean | "always" | { position?: "top" | "bottom" | undefined; when?: "always" | "empty" | undefined; } | undefined;
  /**
   * Fields to filter items by.
   */
  filterFields?: string[] | undefined;
  /**
   * When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
   */
  ignoreFilter?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  ui?: { base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; value?: ClassNameValue; placeholder?: ClassNameValue; arrow?: ClassNameValue; content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; empty?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; input?: ClassNameValue; focusScope?: ClassNameValue; trailingClear?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with listbox
   */
  disabled?: boolean | undefined;
  /**
   * The controlled open state of the Combobox. Can be binded with `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the combobox when it is initially rendered. <br> Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox input blurred
   * @default "true"
   */
  resetSearchTermOnBlur?: boolean | undefined;
  /**
   * Whether to reset the searchTerm when the Combobox value is selected
   * @default "true"
   */
  resetSearchTermOnSelect?: boolean | undefined;
  /**
   * When `true` the `modelValue` will be reset to `null` (or `[]` if `multiple`)
   * @default "true"
   */
  resetModelValueOnClear?: boolean | undefined;
  /**
   * When `true`, hover over item will trigger highlight
   */
  highlightOnHover?: boolean | undefined;
  /**
   * Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
   */
  by?: string | ((a: T, b: T) => boolean) | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
  /**
   * @default "\"\""
   */
  searchTerm?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the SelectMenu component
 */
interface SelectMenuSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  empty(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
  content-top(): any;
  content-bottom(): any;
  create-item-label(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the SelectMenu component
 */
interface SelectMenuEmits {
  update:open: (payload: [value: boolean]) => void;
  change: (payload: [event: Event]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  create: (payload: [item: string]) => void;
  clear: (payload: []) => void;
  highlight: (payload: [payload: { ref: HTMLElement; value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C>; } | undefined]) => void;
  update:modelValue: (payload: [value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, ExcludeItem>, Mod>, Mod>, Mod> | IsClearUsed<M, C>]) => void;
  update:searchTerm: (payload: [value: string]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                                |
| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggerRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}  | `Ref<HTMLButtonElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `viewportRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLDivElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    selectMenu: {
      slots: {
        base: [
          'relative group rounded-md inline-flex items-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leading: 'absolute inset-y-0 start-0 flex items-center',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute inset-y-0 end-0 flex items-center',
        trailingIcon: 'shrink-0 text-dimmed',
        value: 'truncate pointer-events-none',
        placeholder: 'truncate text-dimmed',
        arrow: 'fill-bg stroke-default',
        content: [
          'max-h-60 w-(--reka-select-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-select-content-transform-origin) pointer-events-auto flex flex-col',
          'origin-(--reka-combobox-content-transform-origin) w-(--reka-combobox-trigger-width)'
        ],
        viewport: 'relative scroll-py-1 overflow-y-auto flex-1',
        group: 'p-1 isolate',
        empty: 'text-center text-muted',
        label: 'font-semibold text-highlighted',
        separator: '-mx-1 my-1 h-px bg-border',
        item: [
          'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
          'transition-colors before:transition-colors'
        ],
        itemLeadingIcon: [
          'shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
          'transition-colors'
        ],
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '',
        itemLeadingChip: 'shrink-0',
        itemLeadingChipSize: '',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0',
        itemWrapper: 'flex-1 flex flex-col min-w-0',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-muted',
        input: 'border-b border-default',
        focusScope: 'flex flex-col min-h-0',
        trailingClear: 'p-0'
      },
      variants: {
        fieldGroup: {
          horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]',
          vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]'
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-xs gap-1',
            leading: 'ps-2',
            trailing: 'pe-2',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1 text-[10px]/3 gap-1',
            item: 'p-1 text-xs gap-1',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            empty: 'p-2 text-xs'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-xs gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4',
            label: 'p-1.5 text-[10px]/3 gap-1.5',
            item: 'p-1.5 text-xs gap-1.5',
            itemLeadingIcon: 'size-4',
            itemLeadingAvatarSize: '3xs',
            itemLeadingChip: 'size-4',
            itemLeadingChipSize: 'sm',
            itemTrailingIcon: 'size-4',
            empty: 'p-2.5 text-xs'
          },
          md: {
            base: 'px-2.5 py-1.5 text-sm gap-1.5',
            leading: 'ps-2.5',
            trailing: 'pe-2.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-1.5 text-xs gap-1.5',
            item: 'p-1.5 text-sm gap-1.5',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            empty: 'p-2.5 text-sm'
          },
          lg: {
            base: 'px-3 py-2 text-sm gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5',
            label: 'p-2 text-xs gap-2',
            item: 'p-2 text-sm gap-2',
            itemLeadingIcon: 'size-5',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'size-5',
            itemLeadingChipSize: 'md',
            itemTrailingIcon: 'size-5',
            empty: 'p-3 text-sm'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3',
            trailing: 'pe-3',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6',
            label: 'p-2 text-sm gap-2',
            item: 'p-2 text-base gap-2',
            itemLeadingIcon: 'size-6',
            itemLeadingAvatarSize: 'xs',
            itemLeadingChip: 'size-6',
            itemLeadingChipSize: 'lg',
            itemTrailingIcon: 'size-6',
            empty: 'p-3 text-base'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented hover:bg-elevated disabled:bg-default',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented hover:bg-accented/75 disabled:bg-elevated',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        },
        virtualize: {
          true: {
            viewport: 'p-1 isolate'
          },
          false: {
            viewport: 'divide-y divide-default'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/SelectMenu.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/select-menu.ts).


# Separator

## Usage

Use the Separator component as-is to separate content.

```vue
<template>
  <USeparator />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Separator. Defaults to `horizontal`.

```vue
<template>
  <USeparator orientation="vertical" class="h-48" />
</template>
```

### Label

Use the `label` prop to display a label in the middle of the Separator.

```vue
<template>
  <USeparator label="Hello World" />
</template>
```

### Icon

Use the `icon` prop to display an icon in the middle of the Separator.

```vue
<template>
  <USeparator icon="i-simple-icons-nuxtdotjs" />
</template>
```

### Avatar

Use the `avatar` prop to display an avatar in the middle of the Separator.

```vue
<template>
  <USeparator />
</template>
```

### Color

Use the `color` prop to change the color of the Separator. Defaults to `neutral`.

```vue
<template>
  <USeparator color="primary" type="solid" />
</template>
```

### Type

Use the `type` prop to change the type of the Separator. Defaults to `solid`.

```vue
<template>
  <USeparator type="dashed" />
</template>
```

### Size

Use the `size` prop to change the size of the Separator. Defaults to `xs`.

```vue
<template>
  <USeparator size="lg" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Separator component
 */
interface SeparatorProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  /**
   * Display a label in the middle.
   */
  label?: string | undefined;
  /**
   * Display an icon in the middle.
   */
  icon?: any;
  /**
   * Display an avatar in the middle.
   */
  avatar?: AvatarProps | undefined;
  color?: "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  type?: "solid" | "dashed" | "dotted" | undefined;
  /**
   * The orientation of the separator.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  ui?: { root?: ClassNameValue; border?: ClassNameValue; container?: ClassNameValue; icon?: ClassNameValue; avatar?: ClassNameValue; avatarSize?: ClassNameValue; label?: ClassNameValue; } | undefined;
  /**
   * Whether or not the component is purely decorative. <br>When `true`, accessibility-related attributes
   * are updated so that that the rendered element is removed from the accessibility tree.
   */
  decorative?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Separator component
 */
interface SeparatorSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    separator: {
      slots: {
        root: 'flex items-center align-center text-center',
        border: '',
        container: 'font-medium text-default flex',
        icon: 'shrink-0 size-5',
        avatar: 'shrink-0',
        avatarSize: '2xs',
        label: 'text-sm'
      },
      variants: {
        color: {
          primary: {
            border: 'border-primary'
          },
          secondary: {
            border: 'border-secondary'
          },
          success: {
            border: 'border-success'
          },
          info: {
            border: 'border-info'
          },
          warning: {
            border: 'border-warning'
          },
          error: {
            border: 'border-error'
          },
          neutral: {
            border: 'border-default'
          }
        },
        orientation: {
          horizontal: {
            root: 'w-full flex-row',
            border: 'w-full',
            container: 'mx-3 whitespace-nowrap'
          },
          vertical: {
            root: 'h-full flex-col',
            border: 'h-full',
            container: 'my-2'
          }
        },
        size: {
          xs: '',
          sm: '',
          md: '',
          lg: '',
          xl: ''
        },
        type: {
          solid: {
            border: 'border-solid'
          },
          dashed: {
            border: 'border-dashed'
          },
          dotted: {
            border: 'border-dotted'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          size: 'xs',
          class: {
            border: 'border-t'
          }
        },
        {
          orientation: 'horizontal',
          size: 'sm',
          class: {
            border: 'border-t-[2px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'md',
          class: {
            border: 'border-t-[3px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'lg',
          class: {
            border: 'border-t-[4px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'xl',
          class: {
            border: 'border-t-[5px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'xs',
          class: {
            border: 'border-s'
          }
        },
        {
          orientation: 'vertical',
          size: 'sm',
          class: {
            border: 'border-s-[2px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'md',
          class: {
            border: 'border-s-[3px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'lg',
          class: {
            border: 'border-s-[4px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'xl',
          class: {
            border: 'border-s-[5px]'
          }
        }
      ],
      defaultVariants: {
        color: 'neutral',
        size: 'xs',
        type: 'solid'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Separator.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/separator.ts).


# Sidebar

## Usage

The Sidebar component is a standalone, fixed sidebar that pushes the page content. On desktop, it renders inline and can be collapsed; on mobile, it opens a [Modal](https://ui.nuxt.com/docs/components/modal), [Slideover](https://ui.nuxt.com/docs/components/slideover) or [Drawer](https://ui.nuxt.com/docs/components/drawer) component.

\> \[!TIP]
\> See: /docs/components/dashboard-sidebar
\> Sidebar vs DashboardSidebar: This component is a simple, standalone sidebar you can drop anywhere (chat panel, settings, navigation). If you need drag-to-resize, state persistence and integration with \[DashboardGroup]\(/docs/components/dashboard-group), use \[DashboardSidebar]\(/docs/components/dashboard-sidebar) instead.

Use the `header`, `default` and `footer` slots to customize the sidebar content. The `v-model:open` directive is viewport-aware: on desktop it controls the expanded/collapsed state, on mobile it controls the menu.

```vue [SidebarExample.vue]
<script setup lang="ts">
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'

const open = ref(true)

const colorMode = useColorMode()

const teams = ref([{
  label: 'Nuxt',
  avatar: {
    src: 'https://github.com/nuxt.png',
    alt: 'Nuxt'
  }
}, {
  label: 'Vue',
  avatar: {
    src: 'https://github.com/vuejs.png',
    alt: 'Vue'
  }
}, {
  label: 'UnJS',
  avatar: {
    src: 'https://github.com/unjs.png',
    alt: 'UnJS'
  }
}])
const selectedTeam = ref(teams.value[0])

const teamsItems = computed<DropdownMenuItem[][]>(() => {
  return [teams.value.map((team, index) => ({
    ...team,
    kbds: ['meta', String(index + 1)],
    onSelect() {
      selectedTeam.value = team
    }
  })), [{
    label: 'Create team',
    icon: 'i-lucide-circle-plus'
  }]]
})

function getItems(state: 'collapsed' | 'expanded') {
  return [{
    label: 'Inbox',
    icon: 'i-lucide-inbox',
    badge: '4'
  }, {
    label: 'Issues',
    icon: 'i-lucide-square-dot'
  }, {
    label: 'Activity',
    icon: 'i-lucide-square-activity'
  }, {
    label: 'Settings',
    icon: 'i-lucide-settings',
    defaultOpen: true,
    children: state === 'expanded'
      ? [{
          label: 'General',
          icon: 'i-lucide-house'
        }, {
          label: 'Team',
          icon: 'i-lucide-users'
        }, {
          label: 'Billing',
          icon: 'i-lucide-credit-card'
        }]
      : []
  }] satisfies NavigationMenuItem[]
}

const user = ref({
  name: 'Benjamin Canac',
  avatar: {
    src: 'https://github.com/benjamincanac.png',
    alt: 'Benjamin Canac'
  }
})

const userItems = computed<DropdownMenuItem[][]>(() => ([[{
  label: 'Profile',
  icon: 'i-lucide-user'
}, {
  label: 'Billing',
  icon: 'i-lucide-credit-card'
}, {
  label: 'Settings',
  icon: 'i-lucide-settings',
  to: '/settings'
}], [{
  label: 'Appearance',
  icon: 'i-lucide-sun-moon',
  children: [{
    label: 'Light',
    icon: 'i-lucide-sun',
    type: 'checkbox',
    checked: colorMode.value === 'light',
    onUpdateChecked(checked: boolean) {
      if (checked) {
        colorMode.preference = 'light'
      }
    },
    onSelect(e: Event) {
      e.preventDefault()
    }
  }, {
    label: 'Dark',
    icon: 'i-lucide-moon',
    type: 'checkbox',
    checked: colorMode.value === 'dark',
    onUpdateChecked(checked: boolean) {
      if (checked) {
        colorMode.preference = 'dark'
      }
    },
    onSelect(e: Event) {
      e.preventDefault()
    }
  }]
}], [{
  label: 'GitHub',
  icon: 'i-simple-icons-github',
  to: 'https://github.com/nuxt/ui',
  target: '_blank'
}, {
  label: 'Log out',
  icon: 'i-lucide-log-out'
}]]))

defineShortcuts(extractShortcuts(teamsItems.value))
</script>

<template>
  <div class="flex flex-1">
    <USidebar
      v-model:open="open"
      collapsible="icon"
      rail
      :ui="{
        container: 'h-full',
        inner: 'bg-elevated/25 divide-transparent',
        body: 'py-0'
      }"
    >
      <template #header>
        <UDropdownMenu
          :items="teamsItems"
          :content="{ align: 'start', collisionPadding: 12 }"
          :ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }"
        >
          <UButton
            v-bind="selectedTeam"
            trailing-icon="i-lucide-chevrons-up-down"
            color="neutral"
            variant="ghost"
            square
            class="w-full data-[state=open]:bg-elevated overflow-hidden"
            :ui="{
              trailingIcon: 'text-dimmed ms-auto'
            }"
          />
        </UDropdownMenu>
      </template>

      <template #default="{ state }">
        <UNavigationMenu
          :key="state"
          :items="getItems(state)"
          orientation="vertical"
          :ui="{ link: 'p-1.5 overflow-hidden' }"
        />
      </template>

      <template #footer>
        <UDropdownMenu
          :items="userItems"
          :content="{ align: 'center', collisionPadding: 12 }"
          :ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }"
        >
          <UButton
            v-bind="user"
            :label="user?.name"
            trailing-icon="i-lucide-chevrons-up-down"
            color="neutral"
            variant="ghost"
            square
            class="w-full data-[state=open]:bg-elevated overflow-hidden"
            :ui="{
              trailingIcon: 'text-dimmed ms-auto'
            }"
          />
        </UDropdownMenu>
      </template>
    </USidebar>

    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

### Variant

Use the `variant` prop to change the visual style of the sidebar. Defaults to `sidebar`.

```vue [SidebarPropsExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from '@nuxt/ui'

// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div
    class="flex flex-1"
    :class="[
      variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
      side === 'right' && 'flex-row-reverse'
    ]"
  >
    <USidebar
      v-model:open="open"
      :variant="variant"
      :collapsible="collapsible"
      :side="side"
      :ui="{
        container: 'h-full'
      }"
    >
      <template #header>
        <UIcon name="i-logos-nuxt-icon" class="size-8" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col overflow-hidden lg:peer-data-[variant=floating]:my-4 peer-data-[variant=inset]:m-4 lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:shadow-sm peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-default bg-default">
      <div
        class="h-(--ui-header-height) shrink-0 flex items-center px-4"
        :class="[
          variant !== 'floating' && 'border-b border-default',
          side === 'right' && 'justify-end'
        ]"
      >
        <UButton
          :icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

### Collapsible

Use the `collapsible` prop to change the collapse behavior of the sidebar. Defaults to `offcanvas`.

- `offcanvas`: The sidebar slides out of view completely.
- `icon`: The sidebar shrinks to icon-only width.
- `none`: The sidebar is not collapsible.

```vue [SidebarPropsExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from '@nuxt/ui'

// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div
    class="flex flex-1"
    :class="[
      variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
      side === 'right' && 'flex-row-reverse'
    ]"
  >
    <USidebar
      v-model:open="open"
      :variant="variant"
      :collapsible="collapsible"
      :side="side"
      :ui="{
        container: 'h-full'
      }"
    >
      <template #header>
        <UIcon name="i-logos-nuxt-icon" class="size-8" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col overflow-hidden lg:peer-data-[variant=floating]:my-4 peer-data-[variant=inset]:m-4 lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:shadow-sm peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-default bg-default">
      <div
        class="h-(--ui-header-height) shrink-0 flex items-center px-4"
        :class="[
          variant !== 'floating' && 'border-b border-default',
          side === 'right' && 'justify-end'
        ]"
      >
        <UButton
          :icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

\> \[!TIP]
\> See: #slots
\> You can access the \`state\` in the slot props to customize the content of the sidebar when it is collapsed.

### Side

Use the `side` prop to change the side of the sidebar. Defaults to `left`.

```vue [SidebarPropsExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from '@nuxt/ui'

// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div
    class="flex flex-1"
    :class="[
      variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
      side === 'right' && 'flex-row-reverse'
    ]"
  >
    <USidebar
      v-model:open="open"
      :variant="variant"
      :collapsible="collapsible"
      :side="side"
      :ui="{
        container: 'h-full'
      }"
    >
      <template #header>
        <UIcon name="i-logos-nuxt-icon" class="size-8" />
      </template>

      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col overflow-hidden lg:peer-data-[variant=floating]:my-4 peer-data-[variant=inset]:m-4 lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:shadow-sm peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-default bg-default">
      <div
        class="h-(--ui-header-height) shrink-0 flex items-center px-4"
        :class="[
          variant !== 'floating' && 'border-b border-default',
          side === 'right' && 'justify-end'
        ]"
      >
        <UButton
          :icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

### Title

Use the `title` prop to set the title of the sidebar header.

```vue
<template>
  <USidebar title="Navigation">
    <Placeholder class="h-full" />
  </USidebar>
</template>
```

### Description

Use the `description` prop to set the description of the sidebar header.

```vue
<template>
  <USidebar title="Navigation" description="Browse your workspace">
    <Placeholder class="h-full" />
  </USidebar>
</template>
```

### Rail

Use the `rail` prop to display a thin interactive edge on the sidebar that toggles the collapsed state on click. The rail is only rendered when `collapsible` is not `none`.

```vue
<template>
  <USidebar rail collapsible="icon" title="Navigation">
    <Placeholder class="h-full" />
  </USidebar>
</template>
```

### Close

Use the `close` prop to display a close button in the sidebar header. The close button is only rendered when `collapsible` is not `none`.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <USidebar close rail collapsible="icon" title="Navigation">
    <Placeholder class="h-full" />
  </USidebar>
</template>
```

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<template>
  <USidebar close close-icon="i-lucide-panel-right-close" rail collapsible="icon" side="right" title="Navigation">
    <Placeholder class="h-full" />
  </USidebar>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Mode

Use the `mode` prop to change the mode of the sidebar menu on mobile. Defaults to `slideover`.

```vue [SidebarModeExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from '@nuxt/ui'

// Ignore the props for the example
defineProps<Pick<SidebarProps, 'mode'>>()

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div class="flex flex-1">
    <USidebar v-model:open="open" :mode="mode" title="Navigation">
      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

\> \[!TIP]
\> See: #props
\> You can use the \`menu\` prop to customize the menu of the sidebar, it will adapt depending on the mode you choose.

## Examples

### Control open state

You can control the open state by using the `open` prop or the `v-model:open` directive. On desktop it controls the expanded/collapsed state, on mobile it opens/closes the sheet menu.

```vue [SidebarOpenExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const open = ref(true)

defineShortcuts({
  o: () => open.value = !open.value
})

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div class="flex flex-1">
    <USidebar v-model:open="open" title="Navigation" collapsible="icon">
      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          :aria-label="open ? 'Close sidebar' : 'Open sidebar'"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the open state of the Sidebar by pressing .

### Persist open state

Use [`useLocalStorage`](https://vueuse.org/core/useLocalStorage/){rel="&#x22;nofollow&#x22;"} from VueUse or [`useCookie`](https://nuxt.com/docs/4.x/api/composables/use-cookie){rel="&#x22;nofollow&#x22;"} instead of `ref` to persist the sidebar state across page reloads.

```vue [SidebarPersistExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const open = useLocalStorage('sidebar-open', true)

defineShortcuts({
  o: () => open.value = !open.value
})

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div class="flex flex-1">
    <USidebar v-model:open="open" title="Navigation" collapsible="icon">
      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

\> \[!NOTE]
\> The only difference with the previous example is replacing \`ref(true)\` with \`useLocalStorage('sidebar-open', true)\`.

### With custom width

The sidebar width is controlled by the `--sidebar-width` CSS variable (defaults to `16rem`). The collapsed icon width is controlled by `--sidebar-width-icon` (defaults to `4rem`).

Override them globally in your CSS or per-instance with the `style` attribute.

```vue [SidebarWidthExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div class="flex flex-1">
    <USidebar
      v-model:open="open"
      collapsible="icon"
      :style="{ '--sidebar-width': '20rem' }"
    >
      <UNavigationMenu
        :items="items"
        orientation="vertical"
        :ui="{ link: 'p-1.5 overflow-hidden' }"
      />
    </USidebar>

    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

### With header

To position the sidebar below a [Header](https://ui.nuxt.com/docs/components/header), customize the `gap` and `container` using the `ui` prop.

```vue [SidebarHeaderExample.vue]
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const open = ref(true)

const items: NavigationMenuItem[] = [{
  label: 'Home',
  icon: 'i-lucide-house',
  active: true
}, {
  label: 'Inbox',
  icon: 'i-lucide-inbox',
  badge: '4'
}, {
  label: 'Contacts',
  icon: 'i-lucide-users'
}]
</script>

<template>
  <div class="flex flex-col flex-1">
    <UHeader toggle-side="left" :ui="{ container: 'px-4!' }">
      <template #toggle>
        <UButton
          icon="i-lucide-panel-left"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </template>
    </UHeader>

    <div class="flex flex-1 min-h-0">
      <USidebar
        v-model:open="open"
        collapsible="icon"
        :ui="{
          gap: 'h-[calc(100%-var(--ui-header-height))]',
          container: 'absolute top-(--ui-header-height) bottom-0 h-[calc(100%-var(--ui-header-height))]'
        }"
      >
        <UNavigationMenu
          :items="items"
          orientation="vertical"
          :ui="{ link: 'p-1.5 overflow-hidden' }"
        />
      </USidebar>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>
  </div>
</template>
```

\> \[!NOTE]
\> The \`--ui-header-height\` variable defaults to \`4rem\` and is used by the Header. Adjust it if your navbar uses a different height.

### With AI chat

Use the sidebar on the right side with [ChatMessages](https://ui.nuxt.com/docs/components/chat-messages) and [ChatPrompt](https://ui.nuxt.com/docs/components/chat-prompt) to create an AI chat panel.

```vue [SidebarChatExample.vue]
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { isTextUIPart } from 'ai'
import { Chat } from '@ai-sdk/vue'

const open = ref(true)
const input = ref('')

const messages: UIMessage[] = [{
  id: '1',
  role: 'user',
  parts: [{ type: 'text', text: 'What is Nuxt UI?' }]
}, {
  id: '2',
  role: 'assistant',
  parts: [{ type: 'text', text: 'Nuxt UI is a Vue component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides 125+ accessible components for building modern web apps.' }]
}]

const chat = new Chat({
  messages
})

function onSubmit() {
  if (!input.value.trim()) return

  chat.sendMessage({ text: input.value })

  input.value = ''
}

const ui = {
  prose: {
    p: { base: 'my-2 text-sm/6' },
    li: { base: 'my-0.5 text-sm/6' },
    ul: { base: 'my-2' },
    ol: { base: 'my-2' },
    h1: { base: 'text-xl mb-4' },
    h2: { base: 'text-lg mt-6 mb-3' },
    h3: { base: 'text-base mt-4 mb-2' },
    h4: { base: 'text-sm mt-3 mb-1.5' },
    code: { base: 'text-xs' },
    pre: { root: 'my-2', base: 'text-xs/5' },
    table: { root: 'my-2' },
    hr: { base: 'my-4' }
  }
}
</script>

<template>
  <div class="flex flex-1">
    <div class="flex-1 flex flex-col">
      <div class="h-(--ui-header-height) shrink-0 flex items-center justify-end px-4 border-b border-default">
        <UButton
          icon="i-lucide-panel-right"
          color="neutral"
          variant="ghost"
          aria-label="Toggle sidebar"
          @click="open = !open"
        />
      </div>

      <div class="flex-1 p-4">
        <Placeholder class="size-full" />
      </div>
    </div>

    <USidebar
      v-model:open="open"
      side="right"
      title="AI Chat"
      close
      :style="{ '--sidebar-width': '20rem' }"
      :ui="{ container: 'h-full' }"
    >
      <UTheme :ui="ui">
        <UChatMessages
          :messages="chat.messages"
          :status="chat.status"
          compact
          class="px-0"
        >
          <template #content="{ message }">
            <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
              <MDC
                v-if="isTextUIPart(part)"
                :value="part.text"
                :cache-key="`${message.id}-${index}`"
                class="*:first:mt-0 *:last:mb-0"
              />
            </template>
          </template>
        </UChatMessages>
      </UTheme>

      <template #footer>
        <UChatPrompt
          v-model="input"
          :error="chat.error"
          :autofocus="false"
          variant="subtle"
          size="sm"
          :ui="{ base: 'px-0' }"
          @submit="onSubmit"
        >
          <UChatPromptSubmit
            size="sm"
            :status="chat.status"
            @stop="chat.stop()"
            @reload="chat.regenerate()"
          />
        </UChatPrompt>
      </template>
    </USidebar>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the Sidebar component
 */
interface SidebarProps {
  /**
   * The element or component this component should render as.
   * @default "\"aside\""
   */
  as?: any;
  /**
   * The visual variant of the sidebar.
   * @default "\"sidebar\""
   */
  variant?: "floating" | "sidebar" | "inset" | undefined;
  /**
   * The collapse behavior of the sidebar.
   * - `offcanvas`: The sidebar slides out of view completely.
   * - `icon`: The sidebar shrinks to icon-only width.
   * - `none`: The sidebar is not collapsible.
   * @default "\"offcanvas\""
   */
  collapsible?: "offcanvas" | "icon" | "none" | undefined;
  /**
   * The side to render the sidebar on.
   * @default "\"left\""
   */
  side?: "left" | "right" | undefined;
  /**
   * The title displayed in the sidebar header.
   */
  title?: string | undefined;
  /**
   * The description displayed in the sidebar header.
   */
  description?: string | undefined;
  /**
   * Display a close button to collapse the sidebar.
   * Only renders when `collapsible` is not `none`.
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "false"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * Display a rail on the sidebar edge to toggle collapse.
   * Only renders when `collapsible` is not `none`.
   * @default "false"
   */
  rail?: boolean | undefined;
  /**
   * The mode of the sidebar menu on mobile.
   * @default "\"slideover\" as never"
   */
  mode?: T | undefined;
  /**
   * The props for the sidebar menu component on mobile.
   */
  menu?: SidebarMenu<T> | undefined;
  ui?: { root?: ClassNameValue; gap?: ClassNameValue; container?: ClassNameValue; inner?: ClassNameValue; header?: ClassNameValue; wrapper?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; actions?: ClassNameValue; close?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; rail?: ClassNameValue; } | undefined;
  /**
   * @default "true"
   */
  open?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Sidebar component
 */
interface SidebarSlots {
  header(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
  default(): any;
  footer(): any;
  rail(): any;
  content(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    sidebar: {
      slots: {
        root: 'peer [--sidebar-width:16rem] [--sidebar-width-icon:4rem]',
        gap: 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
        container: 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear lg:flex',
        inner: 'flex size-full flex-col overflow-hidden divide-y divide-default',
        header: 'flex items-center gap-1.5 overflow-hidden px-4 min-h-(--ui-header-height)',
        wrapper: 'min-w-0 flex-1',
        title: 'text-highlighted font-semibold truncate',
        description: 'text-muted text-sm truncate',
        actions: 'flex items-center gap-1.5 shrink-0',
        close: '',
        body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4',
        footer: 'flex items-center gap-1.5 overflow-hidden p-4',
        rail: [
          'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-px lg:flex hover:after:bg-(--ui-border-accented)',
          'after:transition-colors'
        ]
      },
      variants: {
        side: {
          left: {
            container: 'left-0 border-e border-default',
            rail: 'end-0 translate-x-1/2'
          },
          right: {
            container: 'right-0 border-s border-default',
            rail: '-start-px -translate-x-1/2'
          }
        },
        collapsible: {
          offcanvas: {
            root: 'group/sidebar hidden lg:block',
            gap: 'data-[state=collapsed]:w-0'
          },
          icon: {
            root: 'group/sidebar hidden lg:block',
            gap: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
            container: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
            actions: 'group-data-[state=collapsed]/sidebar:hidden',
            body: 'group-data-[state=collapsed]/sidebar:overflow-hidden'
          },
          none: {
            root: 'h-full w-(--sidebar-width)'
          }
        },
        variant: {
          sidebar: {},
          floating: {
            container: 'p-4 border-transparent',
            inner: 'rounded-lg ring ring-default shadow-lg',
            rail: 'inset-y-4'
          },
          inset: {
            container: 'py-4 border-transparent',
            inner: 'divide-transparent',
            rail: 'inset-y-4'
          }
        }
      },
      compoundVariants: [
        {
          side: 'left',
          collapsible: [
            'offcanvas',
            'icon'
          ],
          class: {
            rail: 'cursor-w-resize data-[state=collapsed]:cursor-e-resize'
          }
        },
        {
          side: 'right',
          collapsible: [
            'offcanvas',
            'icon'
          ],
          class: {
            rail: 'cursor-e-resize data-[state=collapsed]:cursor-w-resize'
          }
        },
        {
          side: 'left',
          collapsible: 'none',
          class: {
            root: 'border-e border-default'
          }
        },
        {
          side: 'right',
          collapsible: 'none',
          class: {
            root: 'border-s border-default'
          }
        },
        {
          side: 'left',
          collapsible: 'offcanvas',
          class: {
            container: 'data-[state=collapsed]:-left-(--sidebar-width)'
          }
        },
        {
          side: 'right',
          collapsible: 'offcanvas',
          class: {
            container: 'data-[state=collapsed]:-right-(--sidebar-width)'
          }
        },
        {
          variant: 'floating',
          collapsible: 'icon',
          class: {
            gap: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8))]',
            container: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8)+2px)]'
          }
        },
        {
          variant: 'floating',
          collapsible: 'none',
          class: {
            root: 'p-4 border-0'
          }
        },
        {
          variant: 'inset',
          collapsible: 'none',
          class: {
            root: 'py-4 border-0'
          }
        },
        {
          variant: 'floating',
          side: 'left',
          class: {
            rail: 'end-4'
          }
        },
        {
          variant: 'floating',
          side: 'right',
          class: {
            rail: 'start-[calc(--spacing(4)-1px)]'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Sidebar.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/sidebar.ts).


# Skeleton

## Usage

Use the Skeleton component as-is to display a placeholder.

```vue [SkeletonExample.vue]
<template>
  <div class="flex items-center gap-4">
    <USkeleton class="size-12 rounded-full" />

    <div class="grid gap-2">
      <USkeleton class="h-4 w-[250px]" />
      <USkeleton class="h-4 w-[200px]" />
    </div>
  </div>
</template>
```

## API

### Props

```ts
/**
 * Props for the Skeleton component
 */
interface SkeletonProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  ui?: { base?: any; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Skeleton component
 */
interface SkeletonSlots {
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    skeleton: {
      base: 'animate-pulse rounded-md bg-elevated'
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Skeleton.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/skeleton.ts).


# Slideover

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Slideover.

Then, use the `#content` slot to add the content displayed when the Slideover is open.

```vue
<template>
  <USlideover>
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #content>
      <Placeholder class="h-full m-4" />
    </template></USlideover>
</template>
```

You can also use the `#header`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}, `#body`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} and `#footer`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} slots to customize the Slideover's content.

### Title

Use the `title` prop to set the title of the Slideover's header.

```vue
<template>
  <USlideover title="Slideover with title">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

### Description

Use the `description` prop to set the description of the Slideover's header.

```vue
<template>
  <USlideover title="Slideover with description" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

### Close

Use the `close` prop to customize or hide the close button (with `false` value) displayed in the Slideover's header.

You can pass any property from the [Button](https://ui.nuxt.com/docs/components/button) component to customize it.

```vue
<template>
  <USlideover title="Slideover with close button">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

\> \[!NOTE]
\> The close button is not displayed if the \`#content\` slot is used as it's a part of the header.

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Defaults to `i-lucide-x`.

```vue
<template>
  <USlideover title="Slideover with close button" close-icon="i-lucide-arrow-right">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Side

Use the `side` prop to set the side of the screen where the Slideover will slide in from. Defaults to `right`.

```vue
<template>
  <USlideover side="left" title="Slideover with side">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full min-h-48" />
    </template></USlideover>
</template>
```

### Inset `4.3+`

Use the `inset` prop to inset the Slideover from the edges.

```vue
<template>
  <USlideover side="right" inset title="Slideover with inset">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="min-w-96 min-h-96 size-full" />
    </template></USlideover>
</template>
```

### Transition

Use the `transition` prop to control whether the Slideover is animated or not. Defaults to `true`.

```vue
<template>
  <USlideover :transition="false" title="Slideover without transition">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

### Overlay

Use the `overlay` prop to control whether the Slideover has an overlay or not. Defaults to `true`.

```vue
<template>
  <USlideover :overlay="false" title="Slideover without overlay">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

### Modal

Use the `modal` prop to control whether the Slideover blocks interaction with outside content. Defaults to `true`.

\> \[!NOTE]
\> When \`modal\` is set to \`false\`, the overlay is automatically disabled and outside content becomes interactive.

```vue
<template>
  <USlideover :modal="false" title="Slideover interactive">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

### Dismissible

Use the `dismissible` prop to control whether the Slideover is dismissible when clicking outside of it or pressing escape. Defaults to `true`.

\> \[!NOTE]
\> A \`close\:prevent\` event will be emitted when the user tries to close it.

\> \[!TIP]
\> You can combine \`modal: false\` with \`dismissible: false\` to make the Slideover's background interactive without closing it.

```vue
<template>
  <USlideover :dismissible="false" modal title="Slideover non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />
  
    <template #body>
      <Placeholder class="h-full" />
    </template></USlideover>
</template>
```

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [SlideoverOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <USlideover v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Slideover by pressing .

\> \[!TIP]
\> This allows you to move the trigger outside of the Slideover or remove it entirely.

### Programmatic usage

You can use the [`useOverlay`](https://ui.nuxt.com/docs/composables/use-overlay) composable to open a Slideover programmatically.

\> \[!WARNING]
\> Make sure to wrap your app with the \[\`App\`]\(/docs/components/app) component which uses the \[\`OverlayProvider\`]\(https\://github.com/nuxt/ui/blob/v4/src/runtime/components/OverlayProvider.vue) component.

First, create a slideover component that will be opened programmatically:

```vue [SlideoverExample.vue]
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <USlideover :close="{ onClick: () => emit('close', false) }" :description="`This slideover was opened programmatically ${count} times`">
    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer>
      <div class="flex gap-2">
        <UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <UButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </USlideover>
</template>
```

\> \[!NOTE]
\> We are emitting a \`close\` event when the slideover is closed or dismissed here. You can emit any data through the \`close\` event, however, the event must be emitted in order to capture the return value.

Then, use it in your app:

```vue [SlideoverProgrammaticExample.vue]
<script setup lang="ts">
import { LazySlideoverExample } from '#components'

const count = ref(0)

const toast = useToast()
const overlay = useOverlay()

const slideover = overlay.create(LazySlideoverExample)

async function open() {
  const instance = slideover.open({
    count: count.value
  })

  const shouldIncrement = await instance.result

  if (shouldIncrement) {
    count.value++

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'slideover-success'
    })

    // Update the count
    slideover.patch({
      count: count.value
    })
    return
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'slideover-dismiss'
  })
}
</script>

<template>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
```

\> \[!TIP]
\> You can close the slideover within the slideover component by emitting \`emit('close')\`.

### Nested slideovers

You can nest slideovers within each other.

```vue [SlideoverNestedExample.vue]
<script setup lang="ts">
const first = ref(false)
const second = ref(false)
</script>

<template>
  <USlideover v-model:open="first" title="First slideover" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <USlideover v-model:open="second" title="Second slideover" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #body>
          <Placeholder class="h-full" />
        </template>

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </USlideover>
    </template>
  </USlideover>
</template>
```

### With footer slot

Use the `#footer` slot to add content after the Slideover's body.

```vue [SlideoverFooterSlotExample.vue]
<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <USlideover v-model:open="open" title="Slideover with footer" description="This is useful when you want a form in a Slideover." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </USlideover>
</template>
```

## API

### Props

```ts
/**
 * Props for the Slideover component
 */
interface SlideoverProps {
  title?: string | undefined;
  description?: string | undefined;
  /**
   * The content of the slideover.
   */
  content?: (Omit<DialogContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DialogContentImplEmits>>) | undefined;
  /**
   * Render an overlay behind the slideover.
   * @default "true"
   */
  overlay?: boolean | undefined;
  /**
   * Animate the slideover when opening or closing.
   * @default "true"
   */
  transition?: boolean | undefined;
  /**
   * The side of the slideover.
   * @default "\"right\""
   */
  side?: "right" | "top" | "bottom" | "left" | undefined;
  /**
   * Whether to inset the slideover from the edges.
   */
  inset?: boolean | undefined;
  /**
   * Render the slideover in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * Display a close button to dismiss the slideover.
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default "true"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * When `false`, the slideover will not close when clicking outside or pressing escape.
   * @default "true"
   */
  dismissible?: boolean | undefined;
  ui?: { overlay?: ClassNameValue; content?: ClassNameValue; header?: ClassNameValue; wrapper?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; close?: ClassNameValue; } | undefined;
  /**
   * The controlled open state of the dialog. Can be binded as `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The modality of the dialog When set to `true`, <br>
   * interaction with outside elements will be disabled and only dialog content will be visible to screen readers.
   * @default "true"
   */
  modal?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Slideover component
 */
interface SlideoverSlots {
  default(): any;
  content(): any;
  header(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
  body(): any;
  footer(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Slideover component
 */
interface SlideoverEmits {
  after:leave: (payload: []) => void;
  after:enter: (payload: []) => void;
  close:prevent: (payload: []) => void;
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    slideover: {
      slots: {
        overlay: 'fixed inset-0 bg-elevated/75',
        content: 'fixed bg-default divide-y divide-default sm:ring ring-default sm:shadow-lg flex flex-col focus:outline-none',
        header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
        wrapper: '',
        body: 'flex-1 overflow-y-auto p-4 sm:p-6',
        footer: 'flex items-center gap-1.5 p-4 sm:px-6',
        title: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        close: 'absolute top-4 end-4'
      },
      variants: {
        side: {
          top: {
            content: ''
          },
          right: {
            content: 'max-w-md'
          },
          bottom: {
            content: ''
          },
          left: {
            content: 'max-w-md'
          }
        },
        inset: {
          true: {
            content: 'rounded-lg'
          }
        },
        transition: {
          true: {
            overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
          }
        }
      },
      compoundVariants: [
        {
          side: 'top',
          inset: true,
          class: {
            content: 'max-h-[calc(100%-2rem)] inset-x-4 top-4'
          }
        },
        {
          side: 'top',
          inset: false,
          class: {
            content: 'max-h-full inset-x-0 top-0'
          }
        },
        {
          side: 'right',
          inset: true,
          class: {
            content: 'w-[calc(100%-2rem)] inset-y-4 right-4'
          }
        },
        {
          side: 'right',
          inset: false,
          class: {
            content: 'w-full inset-y-0 right-0'
          }
        },
        {
          side: 'bottom',
          inset: true,
          class: {
            content: 'max-h-[calc(100%-2rem)] inset-x-4 bottom-4'
          }
        },
        {
          side: 'bottom',
          inset: false,
          class: {
            content: 'max-h-full inset-x-0 bottom-0'
          }
        },
        {
          side: 'left',
          inset: true,
          class: {
            content: 'w-[calc(100%-2rem)] inset-y-4 left-4'
          }
        },
        {
          side: 'left',
          inset: false,
          class: {
            content: 'w-full inset-y-0 left-0'
          }
        },
        {
          transition: true,
          side: 'top',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-top_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-top_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'right',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-right_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-right_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'bottom',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-bottom_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'left',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-left_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-left_200ms_ease-in-out]'
          }
        }
      ]
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Slideover.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/slideover.ts).


# Slider

## Usage

Use the `v-model` directive to control the value of the Slider.

```vue
<template>
  <USlider :model-value="50" />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <USlider :default-value="50" />
</template>
```

### Min / Max

Use the `min` and `max` props to set the minimum and maximum values of the Slider. Defaults to `0` and `100`.

```vue
<template>
  <USlider :min="0" :max="50" :default-value="50" />
</template>
```

### Step

Use the `step` prop to set the increment value of the Slider. Defaults to `1`.

```vue
<template>
  <USlider :step="10" :default-value="50" />
</template>
```

### Multiple

Use the `v-model` directive or the `default-value` prop with an array of values to create a range Slider.

```vue
<template>
  <USlider />
</template>
```

Use the `min-steps-between-thumbs` prop to limit the minimum distance between the thumbs.

```vue
<template>
  <USlider :min-steps-between-thumbs="10" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Slider. Defaults to `horizontal`.

```vue
<template>
  <USlider orientation="vertical" :default-value="50" class="h-48" />
</template>
```

### Color

Use the `color` prop to change the color of the Slider.

```vue
<template>
  <USlider color="neutral" :default-value="50" />
</template>
```

### Size

Use the `size` prop to change the size of the Slider.

```vue
<template>
  <USlider size="xl" :default-value="50" />
</template>
```

### Tooltip

Use the `tooltip` prop to display a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) around the Slider thumbs with the current value. You can set it to `true` for default behavior or pass an object to customize it with any property from the [Tooltip](https://ui.nuxt.com/docs/components/tooltip#props) component.

```vue
<template>
  <USlider :default-value="50" tooltip />
</template>
```

### Disabled

Use the `disabled` prop to disable the Slider.

```vue
<template>
  <USlider disabled :default-value="50" />
</template>
```

### Inverted

Use the `inverted` prop to visually invert the Slider.

```vue
<template>
  <USlider inverted :default-value="25" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Slider component
 */
interface SliderProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The orientation of the slider.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * Display a tooltip around the slider thumbs with the current value.
   * `{ disableClosingTrigger: true }`{lang="ts-type"}
   */
  tooltip?: boolean | TooltipProps | undefined;
  /**
   * The value of the slider when initially rendered. Use when you do not need to control the state of the slider.
   */
  defaultValue?: number | number[] | undefined;
  ui?: { root?: ClassNameValue; track?: ClassNameValue; range?: ClassNameValue; thumb?: ClassNameValue; } | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, prevents the user from interacting with the slider.
   */
  disabled?: boolean | undefined;
  /**
   * Whether the slider is visually inverted.
   */
  inverted?: boolean | undefined;
  /**
   * The minimum value for the range.
   * @default "0"
   */
  min?: number | undefined;
  /**
   * The maximum value for the range.
   * @default "100"
   */
  max?: number | undefined;
  /**
   * The stepping interval.
   * @default "1"
   */
  step?: number | undefined;
  /**
   * The minimum permitted steps between multiple thumbs.
   */
  minStepsBetweenThumbs?: number | undefined;
  modelValue?: T | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the Slider component
 */
interface SliderEmits {
  change: (payload: [event: Event]) => void;
  update:modelValue: (payload: [value: T | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    slider: {
      slots: {
        root: 'relative flex items-center select-none touch-none',
        track: 'relative bg-accented overflow-hidden rounded-full grow',
        range: 'absolute rounded-full',
        thumb: 'rounded-full bg-default ring-2 focus-visible:outline-2 focus-visible:outline-offset-2'
      },
      variants: {
        color: {
          primary: {
            range: 'bg-primary',
            thumb: 'ring-primary focus-visible:outline-primary/50'
          },
          secondary: {
            range: 'bg-secondary',
            thumb: 'ring-secondary focus-visible:outline-secondary/50'
          },
          success: {
            range: 'bg-success',
            thumb: 'ring-success focus-visible:outline-success/50'
          },
          info: {
            range: 'bg-info',
            thumb: 'ring-info focus-visible:outline-info/50'
          },
          warning: {
            range: 'bg-warning',
            thumb: 'ring-warning focus-visible:outline-warning/50'
          },
          error: {
            range: 'bg-error',
            thumb: 'ring-error focus-visible:outline-error/50'
          },
          neutral: {
            range: 'bg-inverted',
            thumb: 'ring-inverted focus-visible:outline-inverted/50'
          }
        },
        size: {
          xs: {
            thumb: 'size-3'
          },
          sm: {
            thumb: 'size-3.5'
          },
          md: {
            thumb: 'size-4'
          },
          lg: {
            thumb: 'size-4.5'
          },
          xl: {
            thumb: 'size-5'
          }
        },
        orientation: {
          horizontal: {
            root: 'w-full',
            range: 'h-full'
          },
          vertical: {
            root: 'flex-col h-full',
            range: 'w-full'
          }
        },
        disabled: {
          true: {
            root: 'opacity-75 cursor-not-allowed'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          size: 'xs',
          class: {
            track: 'h-[6px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'sm',
          class: {
            track: 'h-[7px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'md',
          class: {
            track: 'h-[8px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'lg',
          class: {
            track: 'h-[9px]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'xl',
          class: {
            track: 'h-[10px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'xs',
          class: {
            track: 'w-[6px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'sm',
          class: {
            track: 'w-[7px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'md',
          class: {
            track: 'w-[8px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'lg',
          class: {
            track: 'w-[9px]'
          }
        },
        {
          orientation: 'vertical',
          size: 'xl',
          class: {
            track: 'w-[10px]'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Slider.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/slider.ts).


# Stepper

## Usage

Use the Stepper component to display a list of items in a stepper.

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `title?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `content?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `value?: string | number`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, trigger?: ClassNameValue, indicator?: ClassNameValue, icon?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper class="w-full" :items="items" />
</template>
```

\> \[!NOTE]
\> Click on the items to navigate through the steps.

### Color

Use the `color` prop to change the color of the Stepper.

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper color="neutral" class="w-full" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the Stepper.

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper size="xl" class="w-full" :items="items" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Stepper. Defaults to `horizontal`.

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper orientation="vertical" class="w-full" :items="items" />
</template>
```

### Disabled

Use the `disabled` prop to disable navigation through the steps.

```vue
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = ref<StepperItem[]>([
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house',
  },
  {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck',
  },
  {
    title: 'Checkout',
    description: 'Confirm your order',
  },
])
</script>

<template>
  <UStepper disabled :items="items" />
</template>
```

\> \[!NOTE]
\> See: #with-controls
\> This can be useful when you want to force navigation with controls.

## Examples

### With controls

You can add additional controls for the stepper using buttons.

```vue [StepperWithControlsExample.vue]
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items: StepperItem[] = [
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house'
  }, {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck'
  }, {
    title: 'Checkout',
    description: 'Confirm your order'
  }
]

const stepper = useTemplateRef('stepper')
</script>

<template>
  <div class="w-full">
    <UStepper ref="stepper" :items="items">
      <template #content="{ item }">
        <Placeholder class="aspect-video">
          {{ item.title }}
        </Placeholder>
      </template>
    </UStepper>

    <div class="flex gap-2 justify-between mt-4">
      <UButton
        leading-icon="i-lucide-arrow-left"
        :disabled="!stepper?.hasPrev"
        @click="stepper?.prev()"
      >
        Prev
      </UButton>

      <UButton
        trailing-icon="i-lucide-arrow-right"
        :disabled="!stepper?.hasNext"
        @click="stepper?.next()"
      >
        Next
      </UButton>
    </div>
  </div>
</template>
```

### Control active item

You can control the active item by using the `default-value` prop or the `v-model` directive with the `value` of the item. If no `value` is provided, it defaults to the index.

```vue [StepperModelValueExample.vue]
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'
import { onMounted, ref } from 'vue'

const items: StepperItem[] = [
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house'
  }, {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck'
  }, {
    title: 'Checkout',
    description: 'Confirm your order'
  }
]

const active = ref(0)

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    active.value = (active.value + 1) % items.length
  }, 2000)
})
</script>

<template>
  <UStepper v-model="active" :items="items" class="w-full">
    <template #content="{ item }">
      <Placeholder class="aspect-video">
        This is the {{ item?.title }} step.
      </Placeholder>
    </template>
  </UStepper>
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to change the key used to match items when a \`v-model\` or \`default-value\` is provided.

### With content slot

Use the `#content` slot to customize the content of each item.

```vue [StepperContentSlotExample.vue]
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items: StepperItem[] = [
  {
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house'
  }, {
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck'
  }, {
    title: 'Checkout',
    description: 'Confirm your order'
  }
]
</script>

<template>
  <UStepper ref="stepper" :items="items" class="w-full">
    <template #content="{ item }">
      <Placeholder class="aspect-video">
        This is the {{ item?.title }} step.
      </Placeholder>
    </template>
  </UStepper>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [StepperCustomSlotExample.vue]
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'

const items = [
  {
    slot: 'address' as const,
    title: 'Address',
    description: 'Add your address here',
    icon: 'i-lucide-house'
  }, {
    slot: 'shipping' as const,
    title: 'Shipping',
    description: 'Set your preferred shipping method',
    icon: 'i-lucide-truck'
  }, {
    slot: 'checkout' as const,
    title: 'Checkout',
    description: 'Confirm your order'
  }
] satisfies StepperItem[]
</script>

<template>
  <UStepper :items="items" class="w-full">
    <template #address>
      <Placeholder class="aspect-video">
        Address
      </Placeholder>
    </template>

    <template #shipping>
      <Placeholder class="aspect-video">
        Shipping
      </Placeholder>
    </template>

    <template #checkout>
      <Placeholder class="aspect-video">
        Checkout
      </Placeholder>
    </template>
  </UStepper>
</template>
```

## API

### Props

```ts
/**
 * Props for the Stepper component
 */
interface StepperProps {
  items: T[];
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The orientation of the stepper.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * The key used to get the value from the item.
   * @default "\"value\""
   */
  valueKey?: GetItemKeys<T> | undefined;
  /**
   * The value of the step that should be active when initially rendered. Use when you do not need to control the state of the steps.
   */
  defaultValue?: string | number | undefined;
  disabled?: boolean | undefined;
  ui?: { root?: ClassNameValue; header?: ClassNameValue; item?: ClassNameValue; container?: ClassNameValue; trigger?: ClassNameValue; indicator?: ClassNameValue; icon?: ClassNameValue; separator?: ClassNameValue; wrapper?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; content?: ClassNameValue; } | undefined;
  /**
   * Whether or not the steps must be completed in order.
   * @default "true"
   */
  linear?: boolean | undefined;
  modelValue?: string | number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Stepper component
 */
interface StepperSlots {
  indicator(): any;
  wrapper(): any;
  title(): any;
  description(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Stepper component
 */
interface StepperEmits {
  next: (payload: [value: T]) => void;
  prev: (payload: [value: T]) => void;
  update:modelValue: (payload: [value: string | number | undefined]) => void;
}
```

### Expose

You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
const stepper = useTemplateRef('stepper')
</script>

<template>
  <UStepper ref="stepper" />
</template>
```

This will give you access to the following:

| Name                                                                                                                          | Type                                                                                                                               |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `next`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    | `() => void`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}   |
| `prev`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}    | `() => void`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}   |
| `hasNext`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<boolean>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |
| `hasPrev`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<boolean>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    stepper: {
      slots: {
        root: 'flex gap-4',
        header: 'flex',
        item: 'group text-center relative w-full',
        container: 'relative',
        trigger: 'rounded-full font-medium text-center align-middle flex items-center justify-center font-semibold group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted text-muted bg-elevated focus-visible:outline-2 focus-visible:outline-offset-2',
        indicator: 'flex items-center justify-center size-full',
        icon: 'shrink-0',
        separator: 'absolute rounded-full group-data-[disabled]:opacity-75 bg-accented',
        wrapper: '',
        title: 'font-medium text-default',
        description: 'text-muted text-wrap',
        content: 'size-full'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'flex-col',
            container: 'flex justify-center',
            separator: 'top-[calc(50%-2px)] h-0.5',
            wrapper: 'mt-1'
          },
          vertical: {
            header: 'flex-col gap-4',
            item: 'flex text-start',
            separator: 'start-[calc(50%-1px)] -bottom-[10px] w-0.5'
          }
        },
        size: {
          xs: {
            trigger: 'size-6 text-xs',
            icon: 'size-3',
            title: 'text-xs',
            description: 'text-xs',
            wrapper: 'mt-1.5'
          },
          sm: {
            trigger: 'size-8 text-sm',
            icon: 'size-4',
            title: 'text-xs',
            description: 'text-xs',
            wrapper: 'mt-2'
          },
          md: {
            trigger: 'size-10 text-base',
            icon: 'size-5',
            title: 'text-sm',
            description: 'text-sm',
            wrapper: 'mt-2.5'
          },
          lg: {
            trigger: 'size-12 text-lg',
            icon: 'size-6',
            title: 'text-base',
            description: 'text-base',
            wrapper: 'mt-3'
          },
          xl: {
            trigger: 'size-14 text-xl',
            icon: 'size-7',
            title: 'text-lg',
            description: 'text-lg',
            wrapper: 'mt-3.5'
          }
        },
        color: {
          primary: {
            trigger: 'group-data-[state=completed]:bg-primary group-data-[state=active]:bg-primary focus-visible:outline-primary',
            separator: 'group-data-[state=completed]:bg-primary'
          },
          secondary: {
            trigger: 'group-data-[state=completed]:bg-secondary group-data-[state=active]:bg-secondary focus-visible:outline-secondary',
            separator: 'group-data-[state=completed]:bg-secondary'
          },
          success: {
            trigger: 'group-data-[state=completed]:bg-success group-data-[state=active]:bg-success focus-visible:outline-success',
            separator: 'group-data-[state=completed]:bg-success'
          },
          info: {
            trigger: 'group-data-[state=completed]:bg-info group-data-[state=active]:bg-info focus-visible:outline-info',
            separator: 'group-data-[state=completed]:bg-info'
          },
          warning: {
            trigger: 'group-data-[state=completed]:bg-warning group-data-[state=active]:bg-warning focus-visible:outline-warning',
            separator: 'group-data-[state=completed]:bg-warning'
          },
          error: {
            trigger: 'group-data-[state=completed]:bg-error group-data-[state=active]:bg-error focus-visible:outline-error',
            separator: 'group-data-[state=completed]:bg-error'
          },
          neutral: {
            trigger: 'group-data-[state=completed]:bg-inverted group-data-[state=active]:bg-inverted focus-visible:outline-inverted',
            separator: 'group-data-[state=completed]:bg-inverted'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          size: 'xs',
          class: {
            separator: 'start-[calc(50%+16px)] end-[calc(-50%+16px)]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'sm',
          class: {
            separator: 'start-[calc(50%+20px)] end-[calc(-50%+20px)]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'md',
          class: {
            separator: 'start-[calc(50%+28px)] end-[calc(-50%+28px)]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'lg',
          class: {
            separator: 'start-[calc(50%+32px)] end-[calc(-50%+32px)]'
          }
        },
        {
          orientation: 'horizontal',
          size: 'xl',
          class: {
            separator: 'start-[calc(50%+36px)] end-[calc(-50%+36px)]'
          }
        },
        {
          orientation: 'vertical',
          size: 'xs',
          class: {
            separator: 'top-[30px]',
            item: 'gap-1.5'
          }
        },
        {
          orientation: 'vertical',
          size: 'sm',
          class: {
            separator: 'top-[38px]',
            item: 'gap-2'
          }
        },
        {
          orientation: 'vertical',
          size: 'md',
          class: {
            separator: 'top-[46px]',
            item: 'gap-2.5'
          }
        },
        {
          orientation: 'vertical',
          size: 'lg',
          class: {
            separator: 'top-[54px]',
            item: 'gap-3'
          }
        },
        {
          orientation: 'vertical',
          size: 'xl',
          class: {
            separator: 'top-[62px]',
            item: 'gap-3.5'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Stepper.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/stepper.ts).


# Switch

## Usage

Use the `v-model` directive to control the checked state of the Switch.

```vue
<template>
  <USwitch model-value />
</template>
```

Use the `default-value` prop to set the initial value when you do not need to control its state.

```vue
<template>
  <USwitch default-value />
</template>
```

### Label

Use the `label` prop to set the label of the Switch.

```vue
<template>
  <USwitch label="Check me" />
</template>
```

When using the `required` prop, an asterisk is added next to the label.

```vue
<template>
  <USwitch required label="Check me" />
</template>
```

### Description

Use the `description` prop to set the description of the Switch.

```vue
<template>
  <USwitch label="Check me" description="This is a checkbox." />
</template>
```

### Icon

Use the `checked-icon` and `unchecked-icon` props to set the icons of the Switch when checked and unchecked.

```vue
<template>
  <USwitch unchecked-icon="i-lucide-x" checked-icon="i-lucide-check" default-value label="Check me" />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the Switch.

```vue
<template>
  <USwitch loading default-value label="Check me" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <USwitch loading loading-icon="i-lucide-loader" default-value label="Check me" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Color

Use the `color` prop to change the color of the Switch.

```vue
<template>
  <USwitch color="neutral" default-value label="Check me" />
</template>
```

### Size

Use the `size` prop to change the size of the Switch.

```vue
<template>
  <USwitch size="xl" default-value label="Check me" />
</template>
```

### Disabled

Use the `disabled` prop to disable the Switch.

```vue
<template>
  <USwitch disabled label="Check me" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Switch component
 */
interface SwitchProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  /**
   * Display an icon when the switch is checked.
   */
  checkedIcon?: any;
  /**
   * Display an icon when the switch is unchecked.
   */
  uncheckedIcon?: any;
  label?: string | undefined;
  description?: string | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; container?: ClassNameValue; thumb?: ClassNameValue; icon?: ClassNameValue; wrapper?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; } | undefined;
  /**
   * When `true`, prevents the user from interacting with the switch.
   */
  disabled?: boolean | undefined;
  id?: string | undefined;
  /**
   * The name of the field. Submitted with its owning form as part of a name/value pair.
   */
  name?: string | undefined;
  /**
   * When `true`, indicates that the user must set the value before the owning form can be submitted.
   */
  required?: boolean | undefined;
  /**
   * The value given as data when submitted with a `name`.
   */
  value?: string | undefined;
  /**
   * The state of the switch when it is initially rendered. Use when you do not need to control its state.
   */
  defaultValue?: T | undefined;
  /**
   * The controlled state of the switch. Can be bind as `v-model`.
   */
  modelValue?: T | null | undefined;
  /**
   * The value used when the switch is on. Defaults to `true`.
   */
  trueValue?: T | undefined;
  /**
   * The value used when the switch is off. Defaults to `false`.
   */
  falseValue?: T | undefined;
  autofocus?: Booleanish | undefined;
  form?: string | undefined;
  formaction?: string | undefined;
  formenctype?: string | undefined;
  formmethod?: string | undefined;
  formnovalidate?: Booleanish | undefined;
  formtarget?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes
\> This component also supports all native \`\<button>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Switch component
 */
interface SwitchSlots {
  label(): any;
  description(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Switch component
 */
interface SwitchEmits {
  change: (payload: [event: Event]) => void;
  update:modelValue: (payload: [payload: T]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    switch: {
      slots: {
        root: 'relative flex items-start',
        base: [
          'inline-flex items-center shrink-0 rounded-full border-2 border-transparent focus-visible:outline-2 focus-visible:outline-offset-2 data-[state=unchecked]:bg-accented',
          'transition-[background] duration-200'
        ],
        container: 'flex items-center',
        thumb: 'group pointer-events-none rounded-full bg-default shadow-lg ring-0 transition-transform duration-200 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:rtl:-translate-x-0 flex items-center justify-center',
        icon: [
          'absolute shrink-0 group-data-[state=unchecked]:text-dimmed opacity-0 size-10/12',
          'transition-[color,opacity] duration-200'
        ],
        wrapper: 'ms-2',
        label: 'block font-medium text-default',
        description: 'text-muted'
      },
      variants: {
        color: {
          primary: {
            base: 'data-[state=checked]:bg-primary focus-visible:outline-primary',
            icon: 'group-data-[state=checked]:text-primary'
          },
          secondary: {
            base: 'data-[state=checked]:bg-secondary focus-visible:outline-secondary',
            icon: 'group-data-[state=checked]:text-secondary'
          },
          success: {
            base: 'data-[state=checked]:bg-success focus-visible:outline-success',
            icon: 'group-data-[state=checked]:text-success'
          },
          info: {
            base: 'data-[state=checked]:bg-info focus-visible:outline-info',
            icon: 'group-data-[state=checked]:text-info'
          },
          warning: {
            base: 'data-[state=checked]:bg-warning focus-visible:outline-warning',
            icon: 'group-data-[state=checked]:text-warning'
          },
          error: {
            base: 'data-[state=checked]:bg-error focus-visible:outline-error',
            icon: 'group-data-[state=checked]:text-error'
          },
          neutral: {
            base: 'data-[state=checked]:bg-inverted focus-visible:outline-inverted',
            icon: 'group-data-[state=checked]:text-highlighted'
          }
        },
        size: {
          xs: {
            base: 'w-7',
            container: 'h-4',
            thumb: 'size-3 data-[state=checked]:translate-x-3 data-[state=checked]:rtl:-translate-x-3',
            wrapper: 'text-xs'
          },
          sm: {
            base: 'w-8',
            container: 'h-4',
            thumb: 'size-3.5 data-[state=checked]:translate-x-3.5 data-[state=checked]:rtl:-translate-x-3.5',
            wrapper: 'text-xs'
          },
          md: {
            base: 'w-9',
            container: 'h-5',
            thumb: 'size-4 data-[state=checked]:translate-x-4 data-[state=checked]:rtl:-translate-x-4',
            wrapper: 'text-sm'
          },
          lg: {
            base: 'w-10',
            container: 'h-5',
            thumb: 'size-4.5 data-[state=checked]:translate-x-4.5 data-[state=checked]:rtl:-translate-x-4.5',
            wrapper: 'text-sm'
          },
          xl: {
            base: 'w-11',
            container: 'h-6',
            thumb: 'size-5 data-[state=checked]:translate-x-5 data-[state=checked]:rtl:-translate-x-5',
            wrapper: 'text-base'
          }
        },
        checked: {
          true: {
            icon: 'group-data-[state=checked]:opacity-100'
          }
        },
        unchecked: {
          true: {
            icon: 'group-data-[state=unchecked]:opacity-100'
          }
        },
        loading: {
          true: {
            icon: 'animate-spin'
          }
        },
        required: {
          true: {
            label: "after:content-['*'] after:ms-0.5 after:text-error"
          }
        },
        disabled: {
          true: {
            root: 'opacity-75',
            base: 'cursor-not-allowed',
            label: 'cursor-not-allowed',
            description: 'cursor-not-allowed'
          }
        }
      },
      defaultVariants: {
        color: 'primary',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Switch.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/switch.ts).


# Table

## Usage

The Table component is built on top of [TanStack Table](https://tanstack.com/table/latest){rel="&#x22;nofollow&#x22;"} and is powered by the [useVueTable](https://tanstack.com/table/latest/docs/framework/vue/vue-table#usevuetable){rel="&#x22;nofollow&#x22;"} composable to provide a flexible and fully type-safe API.

```vue [TableExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'

const UButton = resolveComponent('UButton')
const UCheckbox = resolveComponent('UCheckbox')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')

const toast = useToast()
const { copy } = useClipboard()

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}, {
  id: '4595',
  date: '2024-03-10T13:40:00',
  status: 'refunded',
  email: 'ava.thomas@example.com',
  amount: 428
}, {
  id: '4594',
  date: '2024-03-10T09:15:00',
  status: 'paid',
  email: 'michael.wilson@example.com',
  amount: 683
}, {
  id: '4593',
  date: '2024-03-09T20:25:00',
  status: 'failed',
  email: 'olivia.taylor@example.com',
  amount: 947
}, {
  id: '4592',
  date: '2024-03-09T18:45:00',
  status: 'paid',
  email: 'benjamin.jackson@example.com',
  amount: 851
}, {
  id: '4591',
  date: '2024-03-09T16:05:00',
  status: 'paid',
  email: 'sophia.miller@example.com',
  amount: 762
}, {
  id: '4590',
  date: '2024-03-09T14:20:00',
  status: 'paid',
  email: 'noah.clark@example.com',
  amount: 573
}, {
  id: '4589',
  date: '2024-03-09T11:35:00',
  status: 'failed',
  email: 'isabella.lee@example.com',
  amount: 389
}, {
  id: '4588',
  date: '2024-03-08T22:50:00',
  status: 'refunded',
  email: 'liam.walker@example.com',
  amount: 701
}, {
  id: '4587',
  date: '2024-03-08T20:15:00',
  status: 'paid',
  email: 'charlotte.hall@example.com',
  amount: 856
}, {
  id: '4586',
  date: '2024-03-08T17:40:00',
  status: 'paid',
  email: 'mason.young@example.com',
  amount: 492
}, {
  id: '4585',
  date: '2024-03-08T14:55:00',
  status: 'failed',
  email: 'amelia.king@example.com',
  amount: 637
}, {
  id: '4584',
  date: '2024-03-08T12:30:00',
  status: 'paid',
  email: 'elijah.wright@example.com',
  amount: 784
}, {
  id: '4583',
  date: '2024-03-08T09:45:00',
  status: 'refunded',
  email: 'harper.scott@example.com',
  amount: 345
}, {
  id: '4582',
  date: '2024-03-07T23:10:00',
  status: 'paid',
  email: 'evelyn.green@example.com',
  amount: 918
}, {
  id: '4581',
  date: '2024-03-07T20:25:00',
  status: 'paid',
  email: 'logan.baker@example.com',
  amount: 567
}])

const columns: TableColumn<Payment>[] = [{
  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'
  }),
  enableSorting: false,
  enableHiding: false
}, {
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: ({ column }) => {
    const isSorted = column.getIsSorted()

    return h(UButton, {
      color: 'neutral',
      variant: 'ghost',
      label: 'Email',
      icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
      class: '-mx-2.5',
      onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
    })
  },
  meta: {
    class: {
      td: 'lowercase'
    }
  }
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}, {
  id: 'actions',
  enableHiding: false,
  meta: {
    class: {
      td: 'text-right'
    }
  },
  cell: ({ row }) => {
    const items = [{
      type: 'label',
      label: 'Actions'
    }, {
      label: 'Copy payment ID',
      onSelect() {
        copy(row.original.id)

        toast.add({
          title: 'Payment ID copied to clipboard!',
          color: 'success',
          icon: 'i-lucide-circle-check'
        })
      }
    }, {
      label: row.getIsExpanded() ? 'Collapse' : 'Expand',
      onSelect() {
        row.toggleExpanded()
      }
    }, {
      type: 'separator'
    }, {
      label: 'View customer'
    }, {
      label: 'View payment details'
    }]

    return h(UDropdownMenu, {
      'content': {
        align: 'end'
      },
      items,
      'aria-label': 'Actions dropdown'
    }, () => h(UButton, {
      'icon': 'i-lucide-ellipsis-vertical',
      'color': 'neutral',
      'variant': 'ghost',
      'aria-label': 'Actions dropdown'
    }))
  }
}]

const table = useTemplateRef('table')

function randomize() {
  data.value = [...data.value].sort(() => Math.random() - 0.5)
}
</script>

<template>
  <div class="flex-1 divide-y divide-accented w-full">
    <div class="flex items-center gap-2 px-4 py-3.5 overflow-x-auto">
      <UInput
        :model-value="(table?.tableApi?.getColumn('email')?.getFilterValue() as string)"
        class="max-w-sm min-w-[12ch]"
        placeholder="Filter emails..."
        @update:model-value="table?.tableApi?.getColumn('email')?.setFilterValue($event)"
      />

      <UButton color="neutral" label="Randomize" @click="randomize" />

      <UDropdownMenu
        :items="table?.tableApi?.getAllColumns().filter(column => column.getCanHide()).map(column => ({
          label: upperFirst(column.id),
          type: 'checkbox' as const,
          checked: column.getIsVisible(),
          onUpdateChecked(checked: boolean) {
            table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
          },
          onSelect(e: Event) {
            e.preventDefault()
          }
        }))"
        :content="{ align: 'end' }"
      >
        <UButton
          label="Columns"
          color="neutral"
          variant="outline"
          trailing-icon="i-lucide-chevron-down"
          class="ml-auto"
          aria-label="Columns select dropdown"
        />
      </UDropdownMenu>
    </div>

    <UTable
      ref="table"
      :data="data"
      :columns="columns"
      sticky
      class="h-96"
    >
      <template #expanded="{ row }">
        <pre>{{ row.original }}</pre>
      </template>
    </UTable>

    <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>
  </div>
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/tree/v4/docs/app/components/content/examples/table/TableExample.vue
\> This example demonstrates the most common use case of the \`Table\` component. Check out the source code on GitHub.

### Data

Use the `data` prop as an array of objects, the columns will be generated based on the keys of the objects.

```vue
<template>
  <UTable class="flex-1" />
</template>
```

### Columns

Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/latest/docs/api/core/column-def){rel="&#x22;nofollow&#x22;"} objects with properties like:

- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{.text-muted}
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{.text-muted}
- [`footer`](https://ui.nuxt.com/#with-column-footer): [The footer to display for the column. Works exactly like header, but is displayed under the table.]{.text-muted}
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{.text-muted}
- `meta`: [Extra properties for the column.]{.text-muted}
  - `class`:
    - `td`: [The classes to apply to the `td` element.]{.text-muted}
    - `th`: [The classes to apply to the `th` element.]{.text-muted}
  - `style`:
    - `td`: [The style to apply to the `td` element.]{.text-muted}
    - `th`: [The style to apply to the `th` element.]{.text-muted}
  - [`colspan`](https://ui.nuxt.com/#with-column-span):
    - `td`: [The colspan attribute to apply to the `td` element.]{.text-muted}
  - [`rowspan`](https://ui.nuxt.com/#with-column-span):
    - `td`: [The rowspan attribute to apply to the `td` element.]{.text-muted}

In order to render components or other HTML elements, you will need to use the Vue [`h` function](https://vuejs.org/api/render-function.html#h){rel="&#x22;nofollow&#x22;"} inside the `header` and `cell` props. This is different from other components that use slots but allows for more flexibility.

\> \[!TIP]
\> See: #with-slots
\> You can also use slots to customize the header and data cells of the table.

```vue [TableColumnsExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>
```

\> \[!NOTE]
\> When rendering components with \`h\`, you can either use the \`resolveComponent\` function or import from \`#components\`.

### Meta

Use the `meta` prop as an object ([TableMeta](https://tanstack.com/table/latest/docs/api/core/table#meta){rel="&#x22;nofollow&#x22;"}) to pass properties like:

- `class`:
  - `tr`: [The classes to apply to the `tr` element.]{.text-muted}
- `style`:
  - `tr`: [The style to apply to the `tr` element.]{.text-muted}

```vue [TableMetaExample.vue]
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { TableMeta, Row } from '@tanstack/vue-table'

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: 'ID',
  meta: {
    class: {
      th: 'text-center font-semibold',
      td: 'text-center font-mono'
    }
  }
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  meta: {
    class: {
      th: 'text-center',
      td: 'text-center'
    }
  },
  cell: ({ row }) => {
    const status = row.getValue('status') as string
    const colorMap = {
      paid: 'text-success',
      failed: 'text-error',
      refunded: 'text-warning'
    }
    return h('span', { class: `font-semibold capitalize ${colorMap[status as keyof typeof colorMap]}` }, status)
  }
}, {
  accessorKey: 'email',
  header: 'Email',
  meta: {
    class: {
      th: 'text-left',
      td: 'text-left'
    }
  }
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right font-bold text-primary',
      td: 'text-right font-mono'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    const formatted = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(amount)
    return h('span', { class: 'font-semibold text-success' }, formatted)
  }
}]

const meta: TableMeta<Payment> = {
  class: {
    tr: (row: Row<Payment>) => {
      if (row.original.status === 'failed') {
        return 'bg-error/10'
      }
      if (row.original.status === 'refunded') {
        return 'bg-warning/10'
      }
      return ''
    }
  }
}
</script>

<template>
  <UTable :data="data" :columns="columns" :meta="meta" class="flex-1" />
</template>
```

### Loading

Use the `loading` prop to display a loading state, the `loading-color` prop to change its color and the `loading-animation` prop to change its animation.

```vue
<template>
  <UTable loading loading-color="primary" loading-animation="carousel" class="flex-1" />
</template>
```

### Sticky

Use the `sticky` prop to make the header or footer sticky.

```vue
<template>
  <UTable sticky class="flex-1 max-h-[312px]" />
</template>
```

## Examples

### With row actions

You can add a new column that renders a [DropdownMenu](https://ui.nuxt.com/docs/components/dropdown-menu) component inside the `cell` to render row actions.

```vue [TableRowActionsExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'
import { useClipboard } from '@vueuse/core'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')

const toast = useToast()
const { copy } = useClipboard()

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}, {
  id: 'actions',
  meta: {
    class: {
      td: 'text-right'
    }
  },
  cell: ({ row }) => {
    return h(UDropdownMenu, {
      'content': {
        align: 'end'
      },
      'items': getRowItems(row),
      'aria-label': 'Actions dropdown'
    }, () => h(UButton, {
      'icon': 'i-lucide-ellipsis-vertical',
      'color': 'neutral',
      'variant': 'ghost',
      'aria-label': 'Actions dropdown'
    }))
  }
}]

function getRowItems(row: Row<Payment>) {
  return [{
    type: 'label',
    label: 'Actions'
  }, {
    label: 'Copy payment ID',
    onSelect() {
      copy(row.original.id)

      toast.add({
        title: 'Payment ID copied to clipboard!',
        color: 'success',
        icon: 'i-lucide-circle-check'
      })
    }
  }, {
    type: 'separator'
  }, {
    label: 'View customer'
  }, {
    label: 'View payment details'
  }]
}
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>
```

### With expandable rows

You can add a new column that renders a [Button](https://ui.nuxt.com/docs/components/button) component inside the `cell` to toggle the expandable state of a row using the TanStack Table [Expanding APIs](https://tanstack.com/table/latest/docs/api/features/expanding){rel="&#x22;nofollow&#x22;"}.

\> \[!CAUTION]
\> You need to define the \`#expanded\` slot to render the expanded content which will receive the row as a parameter.

```vue [TableRowExpandableExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  id: 'expand',
  cell: ({ row }) => h(UButton, {
    'color': 'neutral',
    'variant': 'ghost',
    'icon': 'i-lucide-chevron-down',
    'square': true,
    'aria-label': 'Expand',
    'ui': {
      leadingIcon: ['transition-transform', row.getIsExpanded() ? 'duration-200 rotate-180' : '']
    },
    'onClick': () => row.toggleExpanded()
  })
}, {
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const expanded = ref({ 1: true })
</script>

<template>
  <UTable
    v-model:expanded="expanded"
    :data="data"
    :columns="columns"
    :ui="{ tr: 'data-[expanded=true]:bg-elevated/50' }"
    class="flex-1"
  >
    <template #expanded="{ row }">
      <pre>{{ row.original }}</pre>
    </template>
  </UTable>
</template>
```

\> \[!TIP]
\> You can use the \`expanded\` prop to control the expandable state of the rows (can be bound with \`v-model\`).

\> \[!NOTE]
\> You could also add this action to the \[\`DropdownMenu\`]\(/docs/components/dropdown-menu) component inside the \`actions\` column.

### With grouped rows

You can group rows based on a given column value and show/hide sub rows via some button added to the cell using the TanStack Table [Grouping APIs](https://tanstack.com/table/latest/docs/api/features/grouping){rel="&#x22;nofollow&#x22;"}.

#### Important parts:

- Add `grouping` prop with an array of column ids you want to group by.
- Add `grouping-options` prop. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
- Expand rows via `row.toggleExpanded()` method on any cell of the row. Keep in mind, it also toggles `#expanded` slot.
- Use `aggregateFn` on column definition to define how to aggregate the rows.
- `agregatedCell` renderer on column definition only works if there is no `cell` renderer.

```vue [TableGroupedRowsExample.vue]
<script setup lang="ts">
import { resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import { getGroupedRowModel } from '@tanstack/vue-table'
import type { GroupingOptions } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')

type Account = {
  id: string
  name: string
}

type PaymentStatus = 'paid' | 'failed' | 'refunded'

type Payment = {
  id: string
  date: string
  status: PaymentStatus
  email: string
  amount: number
  account: Account
}

const getColorByStatus = (status: PaymentStatus) => {
  return {
    paid: 'success',
    failed: 'error',
    refunded: 'neutral'
  }[status]
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: 'james.anderson@example.com',
    amount: 594,
    account: {
      id: '1',
      name: 'Account 1'
    }
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: 'mia.white@example.com',
    amount: 276,
    account: {
      id: '2',
      name: 'Account 2'
    }
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: 'william.brown@example.com',
    amount: 315,
    account: {
      id: '1',
      name: 'Account 1'
    }
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: 'emma.davis@example.com',
    amount: 529,
    account: {
      id: '2',
      name: 'Account 2'
    }
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: 'ethan.harris@example.com',
    amount: 639,
    account: {
      id: '1',
      name: 'Account 1'
    }
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'title',
    header: 'Item'
  },
  {
    id: 'account_id',
    accessorKey: 'account.id'
  },
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) =>
      row.getIsGrouped()
        ? `${row.getValue('id')} records`
        : `#${row.getValue('id')}`,
    aggregationFn: 'count'
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    },
    aggregationFn: 'max'
  },
  {
    accessorKey: 'status',
    header: 'Status'
  },
  {
    accessorKey: 'email',
    header: 'Email',
    meta: {
      class: {
        td: 'w-full'
      }
    },
    cell: ({ row }) =>
      row.getIsGrouped()
        ? `${row.getValue('email')} customers`
        : row.getValue('email'),
    aggregationFn: 'uniqueCount'
  },
  {
    accessorKey: 'amount',
    header: 'Amount',
    meta: {
      class: {
        th: 'text-right',
        td: 'text-right font-medium'
      }
    },
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)
    },
    aggregationFn: 'sum'
  }
]

const grouping_options = ref<GroupingOptions>({
  groupedColumnMode: 'remove',
  getGroupedRowModel: getGroupedRowModel()
})
</script>

<template>
  <UTable
    :data="data"
    :columns="columns"
    :grouping="['account_id', 'status']"
    :grouping-options="grouping_options"
    :ui="{
      root: 'min-w-full',
      td: 'empty:p-0' // helps with the colspaned row added for expand slot
    }"
  >
    <template #title-cell="{ row }">
      <div v-if="row.getIsGrouped()" class="flex items-center">
        <span
          class="inline-block"
          :style="{ width: `calc(${row.depth} * 1rem)` }"
        />

        <UButton
          variant="outline"
          color="neutral"
          class="mr-2"
          size="xs"
          :icon="row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus'"
          @click="row.toggleExpanded()"
        />
        <strong v-if="row.groupingColumnId === 'account_id'">{{
          row.original.account.name
        }}</strong>
        <UBadge
          v-else-if="row.groupingColumnId === 'status'"
          :color="getColorByStatus(row.original.status)"
          class="capitalize"
          variant="subtle"
        >
          {{ row.original.status }}
        </UBadge>
      </div>
    </template>
  </UTable>
</template>
```

### With row pinning `Soon`

You can add a column that renders a [Button](https://ui.nuxt.com/docs/components/button) component inside the `cell` to toggle the pinning state of a row using the TanStack Table [Row Pinning APIs](https://tanstack.com/table/latest/docs/api/features/row-pinning){rel="&#x22;nofollow&#x22;"}. Pinned rows will stay at the top or bottom of the table regardless of sorting or filtering.

```vue [TableRowPinningExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { RowPinningState } from '@tanstack/table-core'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}, {
  id: '4595',
  date: '2024-03-10T13:40:00',
  status: 'refunded',
  email: 'ava.thomas@example.com',
  amount: 428
}, {
  id: '4594',
  date: '2024-03-10T09:15:00',
  status: 'paid',
  email: 'michael.wilson@example.com',
  amount: 683
}, {
  id: '4593',
  date: '2024-03-09T20:25:00',
  status: 'failed',
  email: 'olivia.taylor@example.com',
  amount: 947
}, {
  id: '4592',
  date: '2024-03-09T18:45:00',
  status: 'paid',
  email: 'benjamin.jackson@example.com',
  amount: 851
}, {
  id: '4591',
  date: '2024-03-09T16:05:00',
  status: 'paid',
  email: 'sophia.miller@example.com',
  amount: 762
}])

const columns: TableColumn<Payment>[] = [{
  id: 'pin',
  cell: ({ row }) => h(UButton, {
    'icon': 'i-lucide-star',
    'color': row.getIsPinned() ? 'primary' : 'neutral',
    'variant': 'ghost',
    'aria-label': row.getIsPinned() ? 'Unpin row' : 'Pin row to top',
    'onClick': () => {
      if (row.getIsPinned()) {
        row.pin(false)
      } else {
        row.pin('top')
      }
    }
  })
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
      timeZone: 'UTC'
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const rowPinning = ref<RowPinningState>({ top: ['4599', '4597'], bottom: [] })
</script>

<template>
  <UTable
    v-model:row-pinning="rowPinning"
    :data="data"
    :columns="columns"
    :get-row-id="(row: Payment) => row.id"
    class="flex-1 h-96"
  />
</template>
```

\> \[!TIP]
\> You can use the \`row-pinning\` prop to control the pinning state of the rows (can be bound with \`v-model\`).

### With row selection

You can add a new column that renders a [Checkbox](https://ui.nuxt.com/docs/components/checkbox) component inside the `header` and `cell` to select rows using the TanStack Table [Row Selection APIs](https://tanstack.com/table/latest/docs/api/features/row-selection){rel="&#x22;nofollow&#x22;"}.

```vue [TableRowSelectionExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UCheckbox = resolveComponent('UCheckbox')
const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  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'
  })
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const table = useTemplateRef('table')

const rowSelection = ref({ 1: true })
</script>

<template>
  <div class="flex-1 w-full">
    <UTable
      ref="table"
      v-model:row-selection="rowSelection"
      :data="data"
      :columns="columns"
    />

    <div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
      {{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
      {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
    </div>
  </div>
</template>
```

\> \[!TIP]
\> You can use the \`row-selection\` prop to control the selection state of the rows (can be bound with \`v-model\`).

### With row select event

You can add a `@select` listener to make rows clickable with or without a checkbox column.

\> \[!NOTE]
\> The handler function receives the \`Event\` and \`TableRow\` instance as the first and second arguments respectively.

```vue [TableRowSelectEventExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  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'
  })
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const table = useTemplateRef('table')

const rowSelection = ref<Record<string, boolean>>({ })

function onSelect(e: Event, row: TableRow<Payment>) {
  /* If you decide to also select the column you can do this  */
  row.toggleSelected(!row.getIsSelected())
}
</script>

<template>
  <div class="flex w-full flex-1 gap-1">
    <div class="flex-1">
      <UTable
        ref="table"
        v-model:row-selection="rowSelection"
        :data="data"
        :columns="columns"
        @select="onSelect"
      />

      <div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
        {{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
        {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
      </div>
    </div>
  </div>
</template>
```

\> \[!TIP]
\> You can use this to navigate to a page, open a modal or even to select the row manually.

### With row context menu event

You can add a `@contextmenu` listener to make rows right clickable and wrap the Table in a [ContextMenu](https://ui.nuxt.com/docs/components/context-menu) component to display row actions for example.

\> \[!NOTE]
\> The handler function receives the \`Event\` and \`TableRow\` instance as the first and second arguments respectively.

```vue [TableRowContextMenuEventExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

const toast = useToast()
const { copy } = useClipboard()

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  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'
  })
}, {
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const items = ref<ContextMenuItem[]>([])

function getRowItems(row: TableRow<Payment>) {
  return [{
    type: 'label' as const,
    label: 'Actions'
  }, {
    label: 'Copy payment ID',
    onSelect() {
      copy(row.original.id)

      toast.add({
        title: 'Payment ID copied to clipboard!',
        color: 'success',
        icon: 'i-lucide-circle-check'
      })
    }
  }, {
    label: row.getIsExpanded() ? 'Collapse' : 'Expand',
    onSelect() {
      row.toggleExpanded()
    }
  }, {
    type: 'separator' as const
  }, {
    label: 'View customer'
  }, {
    label: 'View payment details'
  }]
}

function onContextmenu(_e: Event, row: TableRow<Payment>) {
  items.value = getRowItems(row)
}
</script>

<template>
  <UContextMenu :items="items">
    <UTable
      :data="data"
      :columns="columns"
      class="flex-1"
      @contextmenu="onContextmenu"
    >
      <template #expanded="{ row }">
        <pre>{{ row.original }}</pre>
      </template>
    </UTable>
  </UContextMenu>
</template>
```

### With row hover event

You can add a `@hover` listener to make rows hoverable and use a [Popover](https://ui.nuxt.com/docs/components/popover) or a [Tooltip](https://ui.nuxt.com/docs/components/tooltip) component to display row details for example.

\> \[!NOTE]
\> The handler function receives the \`Event\` and \`TableRow\` instance as the first and second arguments respectively.

```vue [TableRowHoverEventExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  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'
  })
}, {
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const anchor = ref({ x: 0, y: 0 })

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value
    } as DOMRect)
}))

const open = ref(false)
const openDebounced = refDebounced(open, 10)
const selectedRow = ref<TableRow<Payment> | null>(null)

function onHover(_e: Event, row: TableRow<Payment> | null) {
  selectedRow.value = row

  open.value = !!row
}
</script>

<template>
  <div class="flex w-full flex-1 gap-1">
    <UTable
      :data="data"
      :columns="columns"
      class="flex-1"
      @pointermove="(ev: PointerEvent) => {
        anchor.x = ev.clientX
        anchor.y = ev.clientY
      }"
      @hover="onHover"
    />

    <UPopover
      :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
      :open="openDebounced"
      :reference="reference"
    >
      <template #content>
        <div class="p-4">
          {{ selectedRow?.original?.id }}
        </div>
      </template>
    </UPopover>
  </div>
</template>
```

\> \[!NOTE]
\> This example is similar as the Popover \[with following cursor example]\(/docs/components/popover#with-following-cursor) and uses a \[\`refDebounced\`]\(https\://vueuse.org/shared/refDebounced/#refdebounced) to prevent the Popover from opening and closing too quickly when moving the cursor from one row to another.

### With column footer

You can add a `footer` property to the column definition to render a footer for the column.

```vue [TableColumnFooterExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  footer: ({ column }) => {
    const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
    const formatted = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(total)
    return `Total: ${formatted}`
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>
```

### With column span

You can use the `colspan` and `rowspan` properties in the column `meta` to merge cells. These properties accept a static value or a function that receives the cell and returns the span value.

\> \[!NOTE]
\> When using \`rowspan\`, cells that are "absorbed" by a previous row's span need to be visually hidden. Use the \`class\` meta with a function that returns \`'hidden'\` for those cells.

```vue [TableColumnSpanExample.vue]
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { Cell } from '@tanstack/vue-table'

type Product = {
  category: string
  name: string
  price: number
  stock: number
}

const data = ref<Product[]>([
  { category: 'Electronics', name: 'Laptop', price: 999, stock: 45 },
  { category: 'Electronics', name: 'Phone', price: 699, stock: 120 },
  { category: 'Electronics', name: 'Tablet', price: 499, stock: 78 },
  { category: 'Clothing', name: 'T-Shirt', price: 29, stock: 200 },
  { category: 'Clothing', name: 'Jeans', price: 59, stock: 150 }
])

function getCategoryRowSpan(cell: Cell<Product, unknown>) {
  const category = cell.row.original.category
  const rows = cell.getContext().table.getRowModel().rows
  const rowIndex = rows.findIndex((r: typeof rows[number]) => r.id === cell.row.id)

  if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
    return '1'
  }

  let span = 1
  for (let i = rowIndex + 1; i < rows.length; i++) {
    if (rows[i]!.original.category === category) span++
    else break
  }

  return `${span}`
}

function getCategoryClass(cell: Cell<Product, unknown>) {
  const category = cell.row.original.category
  const rows = cell.getContext().table.getRowModel().rows
  const rowIndex = rows.findIndex((r: typeof rows[number]) => r.id === cell.row.id)

  if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
    return 'hidden'
  }

  return 'font-medium align-middle border-r border-default'
}

const columns: TableColumn<Product>[] = [{
  accessorKey: 'category',
  header: 'Category',
  meta: {
    rowspan: {
      td: getCategoryRowSpan
    },
    class: {
      td: getCategoryClass
    }
  }
}, {
  accessorKey: 'name',
  header: 'Name'
}, {
  accessorKey: 'price',
  header: 'Price',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right'
    }
  },
  cell: ({ row }) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(row.getValue('price'))
  }
}, {
  accessorKey: 'stock',
  header: 'Stock',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right'
    }
  }
}]
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>
```

### With column sorting

You can update a column `header` to render a [Button](https://ui.nuxt.com/docs/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting){rel="&#x22;nofollow&#x22;"}.

```vue [TableColumnSortingExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: ({ column }) => {
    const isSorted = column.getIsSorted()

    return h(UButton, {
      color: 'neutral',
      variant: 'ghost',
      label: 'Email',
      icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
      class: '-mx-2.5',
      onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
    })
  }
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const sorting = ref([{
  id: 'email',
  desc: false
}])
</script>

<template>
  <UTable
    v-model:sorting="sorting"
    :data="data"
    :columns="columns"
    class="flex-1"
  />
</template>
```

\> \[!TIP]
\> You can use the \`sorting\` prop to control the sorting state of the columns (can be bound with \`v-model\`).

You can also create a reusable component to make any column header sortable.

```vue [TableColumnSortingReusableExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: ({ column }) => getHeader(column, 'ID'),
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: ({ column }) => getHeader(column, 'Date'),
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: ({ column }) => getHeader(column, 'Status'),
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: ({ column }) => getHeader(column, 'Email')
}, {
  accessorKey: 'amount',
  header: ({ column }) => getHeader(column, 'Amount'),
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

function getHeader(column: Column<Payment>, label: string) {
  const isSorted = column.getIsSorted()

  return h(UDropdownMenu, {
    'content': {
      align: 'start'
    },
    'aria-label': 'Actions dropdown',
    'items': [{
      label: 'Asc',
      type: 'checkbox',
      icon: 'i-lucide-arrow-up-narrow-wide',
      checked: isSorted === 'asc',
      onSelect: () => {
        if (isSorted === 'asc') {
          column.clearSorting()
        } else {
          column.toggleSorting(false)
        }
      }
    }, {
      label: 'Desc',
      icon: 'i-lucide-arrow-down-wide-narrow',
      type: 'checkbox',
      checked: isSorted === 'desc',
      onSelect: () => {
        if (isSorted === 'desc') {
          column.clearSorting()
        } else {
          column.toggleSorting(true)
        }
      }
    }]
  }, () => h(UButton, {
    'color': 'neutral',
    'variant': 'ghost',
    label,
    'icon': isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
    'class': '-mx-2.5 data-[state=open]:bg-elevated',
    'aria-label': `Sort by ${isSorted === 'asc' ? 'descending' : 'ascending'}`
  }))
}

const sorting = ref([{
  id: 'id',
  desc: false
}])
</script>

<template>
  <UTable
    v-model:sorting="sorting"
    :data="data"
    :columns="columns"
    class="flex-1"
  />
</template>
```

\> \[!NOTE]
\> In this example, we use a function to define the column header but you can also create an actual component.

### With column pinning

You can update a column `header` to render a [Button](https://ui.nuxt.com/docs/components/button) component inside the `header` to toggle the pinning state using the TanStack Table [Pinning APIs](https://tanstack.com/table/latest/docs/api/features/row-pinning){rel="&#x22;nofollow&#x22;"}.

\> \[!NOTE]
\> A pinned column will become sticky on the left or right side of the table. When using column pinning, you should define explicit \`size\` values for your columns to ensure proper column width handling, especially with multiple pinned columns.

```vue [TableColumnPinningExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600000000000000000000000000000000000000',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594000
}, {
  id: '4599000000000000000000000000000000000000',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276000
}, {
  id: '4598000000000000000000000000000000000000',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315000
}, {
  id: '4597000000000000000000000000000000000000',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 5290000
}, {
  id: '4596000000000000000000000000000000000000',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639000
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: ({ column }) => getHeader(column, 'ID', 'left'),
  cell: ({ row }) => `#${row.getValue('id')}`,
  size: 381
}, {
  accessorKey: 'date',
  header: ({ column }) => getHeader(column, 'Date', 'left'),
  size: 172
}, {
  accessorKey: 'status',
  header: ({ column }) => getHeader(column, 'Status', 'left'),
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  },
  size: 103
}, {
  accessorKey: 'email',
  header: ({ column }) => getHeader(column, 'Email', 'left'),
  size: 232
}, {
  accessorKey: 'amount',
  header: ({ column }) => getHeader(column, 'Amount', 'right'),
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  },
  size: 130
}]

function getHeader(column: Column<Payment>, label: string, position: 'left' | 'right') {
  const isPinned = column.getIsPinned()

  return h(UButton, {
    color: 'neutral',
    variant: 'ghost',
    label,
    icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
    class: '-mx-2.5',
    onClick() {
      column.pin(isPinned === position ? false : position)
    }
  })
}

const columnPinning = ref({
  left: ['id'],
  right: ['amount']
})
</script>

<template>
  <UTable
    v-model:column-pinning="columnPinning"
    :data="data"
    :columns="columns"
    class="flex-1"
  />
</template>
```

\> \[!TIP]
\> You can use the \`column-pinning\` prop to control the pinning state of the columns (can be bound with \`v-model\`).

### With column visibility

You can use a [DropdownMenu](https://ui.nuxt.com/docs/components/dropdown-menu) component to toggle the visibility of the columns using the TanStack Table [Column Visibility APIs](https://tanstack.com/table/latest/docs/api/features/column-visibility){rel="&#x22;nofollow&#x22;"}.

```vue [TableColumnVisibilityExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const table = useTemplateRef('table')

const columnVisibility = ref({
  id: false
})
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex justify-end px-4 py-3.5 border-b border-accented">
      <UDropdownMenu
        :items="table?.tableApi?.getAllColumns().filter(column => column.getCanHide()).map(column => ({
          label: upperFirst(column.id),
          type: 'checkbox' as const,
          checked: column.getIsVisible(),
          onUpdateChecked(checked: boolean) {
            table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
          },
          onSelect(e: Event) {
            e.preventDefault()
          }
        }))"
        :content="{ align: 'end' }"
      >
        <UButton
          label="Columns"
          color="neutral"
          variant="outline"
          trailing-icon="i-lucide-chevron-down"
        />
      </UDropdownMenu>
    </div>

    <UTable
      ref="table"
      v-model:column-visibility="columnVisibility"
      :data="data"
      :columns="columns"
    />
  </div>
</template>
```

\> \[!TIP]
\> You can use the \`column-visibility\` prop to control the visibility state of the columns (can be bound with \`v-model\`).

### With column filters

You can use an [Input](https://ui.nuxt.com/docs/components/input) component to filter per column the rows using the TanStack Table [Column Filtering APIs](https://tanstack.com/table/latest/docs/api/features/column-filtering){rel="&#x22;nofollow&#x22;"}.

```vue [TableColumnFiltersExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const table = useTemplateRef('table')

const columnFilters = ref([{
  id: 'email',
  value: 'james'
}])
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput
        :model-value="(table?.tableApi?.getColumn('email')?.getFilterValue() as string)"
        class="max-w-sm"
        placeholder="Filter emails..."
        @update:model-value="table?.tableApi?.getColumn('email')?.setFilterValue($event)"
      />
    </div>

    <UTable
      ref="table"
      v-model:column-filters="columnFilters"
      :data="data"
      :columns="columns"
    />
  </div>
</template>
```

\> \[!TIP]
\> You can use the \`column-filters\` prop to control the filters state of the columns (can be bound with \`v-model\`).

### With global filter

You can use an [Input](https://ui.nuxt.com/docs/components/input) component to filter the rows using the TanStack Table [Global Filtering APIs](https://tanstack.com/table/latest/docs/api/features/global-filtering){rel="&#x22;nofollow&#x22;"}.

```vue [TableGlobalFilterExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const globalFilter = ref('45')
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput
        v-model="globalFilter"
        class="max-w-sm"
        placeholder="Filter..."
      />
    </div>

    <UTable
      ref="table"
      v-model:global-filter="globalFilter"
      :data="data"
      :columns="columns"
    />
  </div>
</template>
```

\> \[!TIP]
\> You can use the \`global-filter\` prop to control the global filter state (can be bound with \`v-model\`).

### With pagination

You can use a [Pagination](https://ui.nuxt.com/docs/components/pagination) component to control the pagination state using the [Pagination APIs](https://tanstack.com/table/latest/docs/api/features/pagination){rel="&#x22;nofollow&#x22;"}.

There are different pagination approaches as explained in [Pagination Guide](https://tanstack.com/table/latest/docs/guide/pagination#pagination-guide){rel="&#x22;nofollow&#x22;"}. In this example, we use client-side pagination so we need to manually pass `getPaginationRowModel()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} function.

```vue [TablePaginationExample.vue]
<script setup lang="ts">
import { getPaginationRowModel } from '@tanstack/vue-table'
import type { TableColumn } from '@nuxt/ui'

const table = useTemplateRef('table')

type Payment = {
  id: string
  date: string
  email: string
  amount: number
}
const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  email: 'ethan.harris@example.com',
  amount: 639
}, {
  id: '4595',
  date: '2024-03-10T13:20:00',
  email: 'sophia.miller@example.com',
  amount: 428
}, {
  id: '4594',
  date: '2024-03-10T11:05:00',
  email: 'noah.wilson@example.com',
  amount: 673
}, {
  id: '4593',
  date: '2024-03-09T22:15:00',
  email: 'olivia.jones@example.com',
  amount: 382
}, {
  id: '4592',
  date: '2024-03-09T20:30:00',
  email: 'liam.taylor@example.com',
  amount: 547
}, {
  id: '4591',
  date: '2024-03-09T18:45:00',
  email: 'ava.thomas@example.com',
  amount: 291
}, {
  id: '4590',
  date: '2024-03-09T16:20:00',
  email: 'lucas.martin@example.com',
  amount: 624
}, {
  id: '4589',
  date: '2024-03-09T14:10:00',
  email: 'isabella.clark@example.com',
  amount: 438
}, {
  id: '4588',
  date: '2024-03-09T12:05:00',
  email: 'mason.rodriguez@example.com',
  amount: 583
}, {
  id: '4587',
  date: '2024-03-09T10:30:00',
  email: 'sophia.lee@example.com',
  amount: 347
}, {
  id: '4586',
  date: '2024-03-09T08:15:00',
  email: 'ethan.walker@example.com',
  amount: 692
}, {
  id: '4585',
  date: '2024-03-08T23:40:00',
  email: 'amelia.hall@example.com',
  amount: 419
}, {
  id: '4584',
  date: '2024-03-08T21:25:00',
  email: 'oliver.young@example.com',
  amount: 563
}, {
  id: '4583',
  date: '2024-03-08T19:50:00',
  email: 'aria.king@example.com',
  amount: 328
}, {
  id: '4582',
  date: '2024-03-08T17:35:00',
  email: 'henry.wright@example.com',
  amount: 647
}, {
  id: '4581',
  date: '2024-03-08T15:20:00',
  email: 'luna.lopez@example.com',
  amount: 482
}])
const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const pagination = ref({
  pageIndex: 0,
  pageSize: 5
})

const globalFilter = ref('')
</script>

<template>
  <div class="w-full space-y-4 pb-4">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput
        v-model="globalFilter"
        class="max-w-sm"
        placeholder="Filter..."
      />
    </div>

    <UTable
      ref="table"
      v-model:pagination="pagination"
      v-model:global-filter="globalFilter"
      :data="data"
      :columns="columns"
      :pagination-options="{
        getPaginationRowModel: getPaginationRowModel()
      }"
      class="flex-1"
    />

    <div class="flex justify-end border-t border-default pt-4 px-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>
  </div>
</template>
```

\> \[!TIP]
\> You can use the \`pagination\` prop to control the pagination state (can be bound with \`v-model\`).

### With fetched data

You can fetch data from an API and use them in the Table.

```vue [TableFetchExample.vue]
<script setup lang="ts">
import type { AvatarProps, TableColumn } from '@nuxt/ui'

const UAvatar = resolveComponent('UAvatar')

type User = {
  id: number
  name: string
  username: string
  email: string
  avatar: AvatarProps
  company: { name: string }
}

const { data, status } = useLazyFetch<User[]>('https://jsonplaceholder.typicode.com/users', {
  key: 'table-users',
  transform: (data) => {
    return data?.map(user => ({
      ...user,
      avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, alt: `${user.name} avatar` }
    })) || []
  },
  server: false
})

const columns: TableColumn<User>[] = [{
  accessorKey: 'id',
  header: 'ID'
}, {
  accessorKey: 'name',
  header: 'Name',
  cell: ({ row }) => {
    return h('div', { class: 'flex items-center gap-3' }, [
      h(UAvatar, {
        ...row.original.avatar,
        loading: 'lazy',
        size: 'lg'
      }),
      h('div', undefined, [
        h('p', { class: 'font-medium text-highlighted' }, row.original.name),
        h('p', { class: '' }, `@${row.original.username}`)
      ])
    ])
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'company',
  header: 'Company',
  cell: ({ row }) => row.original.company.name
}]
</script>

<template>
  <UTable
    :data="data"
    :columns="columns"
    :loading="status === 'pending' || status === 'idle'"
    class="flex-1 h-80"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`server: false\` to fetch data on the client without blocking the initial render. The loading state checks for both \`pending\` and \`idle\` status to display a loading indicator before and during the fetch.

### With infinite scroll

If you use server-side pagination, you can use the [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/#useinfinitescroll){rel="&#x22;nofollow&#x22;"} composable to load more data as the user scrolls.

```vue [TableInfiniteScrollExample.vue]
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useInfiniteScroll } from '@vueuse/core'

const UAvatar = resolveComponent('UAvatar')

type User = {
  id: number
  firstName: string
  username: string
  email: string
  image: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status } = useLazyFetch('https://dummyjson.com/users?limit=10&select=firstName,username,email,image', {
  key: 'table-users-infinite-scroll',
  params: { skip },
  transform: (data?: UserResponse) => {
    return data?.users
  },
  server: false
})

const columns: TableColumn<User>[] = [{
  accessorKey: 'id',
  header: 'ID'
}, {
  accessorKey: 'image',
  header: 'Avatar',
  cell: ({ row }) => h(UAvatar, { src: row.original.image, loading: 'lazy' })
}, {
  accessorKey: 'firstName',
  header: 'First name'
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'username',
  header: 'Username'
}]

const users = ref<User[]>([])

watch(data, () => {
  users.value = [
    ...users.value,
    ...(data.value || [])
  ]
})

const table = useTemplateRef('table')

onMounted(() => {
  useInfiniteScroll(table.value?.$el, () => {
    skip.value += 10
  }, {
    distance: 200,
    canLoadMore: () => {
      return status.value !== 'pending'
    }
  })
})
</script>

<template>
  <UTable
    ref="table"
    :data="users"
    :columns="columns"
    :loading="status === 'pending' || status === 'idle'"
    sticky
    class="flex-1 h-80"
  />
</template>
```

\> \[!NOTE]
\> This example uses \`useLazyFetch\` with \`server: false\` to fetch data on the client without blocking the initial render. The loading state checks for both \`pending\` and \`idle\` status to display a loading indicator before and during the fetch. Additional pages are loaded as the user scrolls.

### With drag and drop

You can use the [`useSortable`](https://vueuse.org/integrations/useSortable/){rel="&#x22;nofollow&#x22;"} composable from [`@vueuse/integrations`](https://vueuse.org/integrations/README.html){rel="&#x22;nofollow&#x22;"} to enable drag and drop functionality on the Table. This integration wraps [Sortable.js](https://sortablejs.github.io/Sortable/){rel="&#x22;nofollow&#x22;"} to provide a seamless drag and drop experience.

\> \[!NOTE]
\> Since the table ref doesn't expose the tbody element, add a unique class to it via the \`:ui\` prop to target it with \`useSortable\` (e.g. \`:ui="{ tbody: 'my-table-tbody' }"\`).

```vue [TableDragAndDropExample.vue]
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'

type Payment = {
  id: string
  date: string
  email: string
  amount: number
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  email: 'emma.davis@example.com',
  amount: 529
}])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

useSortable('.my-table-tbody', data, {
  animation: 150
})
</script>

<template>
  <UTable
    ref="table"
    :data="data"
    :columns="columns"
    :ui="{
      tbody: 'my-table-tbody'
    }"
    class="flex-1"
  />
</template>
```

### With virtualization `4.1+`

Use the `virtualize` prop to enable virtualization for large datasets as a boolean or an object with options like `{ estimateSize: 65, overscan: 12 }`. You can also pass other [TanStack Virtual options](https://tanstack.com/virtual/latest/docs/api/virtualizer#optional-options){rel="&#x22;nofollow&#x22;"} to customize the virtualization behavior.

\> \[!WARNING]
\> When virtualization is enabled, the divider between rows and sticky properties are not supported.

```vue [TableVirtualizeExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>(Array(1000).fill(0).map((_, i) => ({
  id: `4600-${i}`,
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
})))

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]
</script>

<template>
  <UTable
    virtualize
    :data="data"
    :columns="columns"
    class="flex-1 h-80"
  />
</template>
```

\> \[!NOTE]
\> A height constraint is required on the table for virtualization to work properly (e.g., \`class="h-\[400px]"\`).

### With tree data

You can use the `get-sub-rows` prop to display hierarchical (tree) data in the table.
For example, if your data objects have a `children` array, set `:get-sub-rows="row => row.children"` to enable expandable rows.

```vue [TableTreeDataExample.vue]
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UCheckbox = resolveComponent('UCheckbox')
const UButton = resolveComponent('UButton')

type Payment = {
  id: string
  date: string
  email: string
  amount: number
  children?: Payment[]
}

const data = ref<Payment[]>([{
  id: '4600',
  date: '2024-03-11T15:30:00',
  email: 'james.anderson@example.com',
  amount: 594,
  children: [
    {
      id: '4599',
      date: '2024-03-11T10:10:00',
      email: 'mia.white@example.com',
      amount: 276
    }, {
      id: '4598',
      date: '2024-03-11T08:50:00',
      email: 'william.brown@example.com',
      amount: 315
    }, {
      id: '4597',
      date: '2024-03-10T19:45:00',
      email: 'emma.davis@example.com',
      amount: 529,
      children: [
        {
          id: '4592',
          date: '2024-03-09T18:45:00',
          email: 'benjamin.jackson@example.com',
          amount: 851
        }, {
          id: '4591',
          date: '2024-03-09T16:05:00',
          email: 'sophia.miller@example.com',
          amount: 762
        }, {
          id: '4590',
          date: '2024-03-09T14:20:00',
          email: 'noah.clark@example.com',
          amount: 573,
          children: [
            {
              id: '4596',
              date: '2024-03-10T15:55:00',
              email: 'ethan.harris@example.com',
              amount: 639
            }, {
              id: '4595',
              date: '2024-03-10T13:40:00',
              email: 'ava.thomas@example.com',
              amount: 428
            }
          ]
        }
      ]
    }
  ]
}, {
  id: '4589',
  date: '2024-03-09T11:35:00',
  email: 'isabella.lee@example.com',
  amount: 389
}])

const columns: TableColumn<Payment>[] = [{
  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() ? true : row.getIsSomeSelected() ? 'indeterminate' : false,
    'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
    'aria-label': 'Select row'
  })
}, {
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => {
    return h(
      'div',
      {
        style: {
          paddingLeft: `${row.depth}rem`
        },
        class: 'flex items-center gap-2'
      },
      [
        h(UButton, {
          color: 'neutral',
          variant: 'outline',
          size: 'xs',
          icon: row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus',
          class: !row.getCanExpand() && 'invisible',
          ui: {
            base: 'p-0 rounded-sm',
            leadingIcon: 'size-4'
          },
          onClick: row.getToggleExpandedHandler()
        }),
        row.getValue('id') as string
      ]
    )
  }
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'EUR'
    }).format(amount)
  }
}]

const expanded = ref({ 0: true })
</script>

<template>
  <UTable
    v-model:expanded="expanded"
    :data="data"
    :columns="columns"
    :get-sub-rows="row => row.children"
    class="flex-1"
    :ui="{
      base: 'border-separate border-spacing-0',
      tbody: '[&>tr]:last:[&>td]:border-b-0',
      tr: 'group',
      td: 'empty:p-0 group-has-[td:not(:empty)]:border-b border-default'
    }"
  />
</template>
```

### With slots

You can use slots to customize the header and data cells of the table.

Use the `#<column>-header` slot to customize the header of a column. You will have access to the `column`, `header` and `table` properties in the slot scope.

Use the `#<column>-cell` slot to customize the cell of a column. You will have access to the `cell`, `column`, `getValue`, `renderValue`, `row`, and `table` properties in the slot scope.

```vue [TableSlotsExample.vue]
<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'

interface User {
  id: number
  name: string
  position: string
  email: string
  role: string
}

const toast = useToast()
const { copy } = useClipboard()

const data = ref<User[]>([{
  id: 1,
  name: 'Lindsay Walton',
  position: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  position: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  position: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  position: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  position: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  position: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}])

const columns: TableColumn<User>[] = [{
  accessorKey: 'id',
  header: 'ID'
}, {
  accessorKey: 'name',
  header: 'Name'
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'role',
  header: 'Role'
}, {
  id: 'action'
}]

function getDropdownActions(user: User): DropdownMenuItem[][] {
  return [
    [{
      label: 'Copy user Id',
      icon: 'i-lucide-copy',
      onSelect: () => {
        copy(user.id.toString())

        toast.add({
          title: 'User ID copied to clipboard!',
          color: 'success',
          icon: 'i-lucide-circle-check'
        })
      }
    }],
    [{
      label: 'Edit',
      icon: 'i-lucide-edit'
    }, {
      label: 'Delete',
      icon: 'i-lucide-trash',
      color: 'error'
    }]
  ]
}
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1">
    <template #name-cell="{ row }">
      <div class="flex items-center gap-3">
        <UAvatar :src="`https://i.pravatar.cc/120?img=${row.original.id}`" size="lg" loading="lazy" :alt="`${row.original.name} avatar`" />
        <div>
          <p class="font-medium text-highlighted">
            {{ row.original.name }}
          </p>
          <p>
            {{ row.original.position }}
          </p>
        </div>
      </div>
    </template>
    <template #action-cell="{ row }">
      <UDropdownMenu :items="getDropdownActions(row.original)">
        <UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" aria-label="Actions" />
      </UDropdownMenu>
    </template>
  </UTable>
</template>
```

## API

### Props

```ts
/**
 * Props for the Table component
 */
interface TableProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  data?: T[] | undefined;
  columns?: TableColumn<T, unknown>[] | undefined;
  caption?: string | undefined;
  /**
   * You can pass any object to `options.meta` and access it anywhere the `table` is available via `table.options.meta`.
   */
  meta?: TableMeta<T> | undefined;
  /**
   * Enable virtualization for large datasets.
   * Note: when enabled, the divider between rows, sticky and row pinning properties are not supported.
   * @default "false"
   */
  virtualize?: boolean | (Partial<Omit<VirtualizerOptions<Element, Element>, "getScrollElement" | "count" | "estimateSize" | "overscan">> & { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; }) | undefined;
  /**
   * The text to display when the table is empty.
   */
  empty?: string | undefined;
  /**
   * Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only.
   * Note: this prop is not supported when `virtualize` is true.
   */
  sticky?: boolean | "header" | "footer" | undefined;
  /**
   * Whether the table should be in loading state.
   */
  loading?: boolean | undefined;
  loadingColor?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  loadingAnimation?: "carousel" | "carousel-inverse" | "swing" | "elastic" | undefined;
  /**
   * Use the `watchOptions` prop to customize reactivity (for ex: disable deep watching for changes in your data or limiting the max traversal depth). This can improve performance by reducing unnecessary re-renders, but it should be used with caution as it may lead to unexpected behavior if not managed properly.
   * @default "{
    deep: true
}"
   */
  watchOptions?: WatchOptions<boolean> | undefined;
  globalFilterOptions?: Omit<GlobalFilterOptions<T>, "onGlobalFilterChange"> | undefined;
  columnFiltersOptions?: Omit<ColumnFiltersOptions<T>, "getFilteredRowModel" | "onColumnFiltersChange"> | undefined;
  columnPinningOptions?: Omit<ColumnPinningOptions, "onColumnPinningChange"> | undefined;
  columnSizingOptions?: Omit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange"> | undefined;
  visibilityOptions?: Omit<VisibilityOptions, "onColumnVisibilityChange"> | undefined;
  sortingOptions?: Omit<SortingOptions<T>, "getSortedRowModel" | "onSortingChange"> | undefined;
  groupingOptions?: Omit<GroupingOptions, "onGroupingChange"> | undefined;
  expandedOptions?: Omit<ExpandedOptions<T>, "getExpandedRowModel" | "onExpandedChange"> | undefined;
  rowSelectionOptions?: Omit<RowSelectionOptions<T>, "onRowSelectionChange"> | undefined;
  rowPinningOptions?: Omit<RowPinningOptions<T>, "onRowPinningChange"> | undefined;
  paginationOptions?: Omit<PaginationOptions, "onPaginationChange"> | undefined;
  facetedOptions?: FacetedOptions<T> | undefined;
  onSelect?: ((e: Event, row: TableRow<T>) => void) | undefined;
  onHover?: ((e: Event, row: TableRow<T> | null) => void) | undefined;
  onContextmenu?: ((e: Event, row: TableRow<T>) => void) | ((e: Event, row: TableRow<T>) => void)[] | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; caption?: ClassNameValue; thead?: ClassNameValue; tbody?: ClassNameValue; tfoot?: ClassNameValue; tr?: ClassNameValue; th?: ClassNameValue; td?: ClassNameValue; separator?: ClassNameValue; empty?: ClassNameValue; loading?: ClassNameValue; } | undefined;
  state?: Partial<TableState> | undefined;
  onStateChange?: ((updater: Updater<TableState>) => void) | undefined;
  renderFallbackValue?: any;
  /**
   * An array of extra features that you can add to the table instance.
   */
  _features?: TableFeature<any>[] | undefined;
  /**
   * Set this option to override any of the `autoReset...` feature options.
   */
  autoResetAll?: boolean | undefined;
  /**
   * Set this option to `true` to output all debugging information to the console.
   */
  debugAll?: boolean | undefined;
  /**
   * Set this option to `true` to output cell debugging information to the console.
   */
  debugCells?: boolean | undefined;
  /**
   * Set this option to `true` to output column debugging information to the console.
   */
  debugColumns?: boolean | undefined;
  /**
   * Set this option to `true` to output header debugging information to the console.
   */
  debugHeaders?: boolean | undefined;
  /**
   * Set this option to `true` to output row debugging information to the console.
   */
  debugRows?: boolean | undefined;
  /**
   * Set this option to `true` to output table debugging information to the console.
   */
  debugTable?: boolean | undefined;
  /**
   * Default column options to use for all column defs supplied to the table.
   */
  defaultColumn?: Partial<ColumnDef<T, unknown>> | undefined;
  /**
   * This optional function is used to derive a unique ID for any given row. If not provided the rows index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc.
   */
  getRowId?: ((originalRow: T, index: number, parent?: Row<T> | undefined) => string) | undefined;
  /**
   * This optional function is used to access the sub rows for any given row. If you are using nested rows, you will need to use this function to return the sub rows object (or undefined) from the row.
   */
  getSubRows?: ((originalRow: T, index: number) => T[] | undefined) | undefined;
  /**
   * Use this option to optionally pass initial state to the table. This state will be used when resetting various table states either automatically by the table (eg. `options.autoResetPageIndex`) or via functions like `table.resetRowSelection()`. Most reset function allow you optionally pass a flag to reset to a blank/default state instead of the initial state.
   * 
   * Table state will not be reset when this object changes, which also means that the initial state object does not need to be stable.
   */
  initialState?: InitialTableState | undefined;
  /**
   * This option is used to optionally implement the merging of table options.
   */
  mergeOptions?: ((defaultOptions: TableOptions<T>, options: Partial<TableOptions<T>>) => TableOptions<T>) | undefined;
  cellpadding?: Numberish | undefined;
  cellspacing?: Numberish | undefined;
  summary?: string | undefined;
  width?: Numberish | undefined;
  globalFilter?: string | undefined;
  columnFilters?: ColumnFiltersState | undefined;
  columnOrder?: ColumnOrderState | undefined;
  columnVisibility?: VisibilityState | undefined;
  columnPinning?: ColumnPinningState | undefined;
  columnSizing?: ColumnSizingState | undefined;
  columnSizingInfo?: ColumnSizingInfoState | undefined;
  rowSelection?: RowSelectionState | undefined;
  rowPinning?: RowPinningState | undefined;
  sorting?: SortingState | undefined;
  grouping?: GroupingState | undefined;
  expanded?: ExpandedState | undefined;
  pagination?: PaginationState | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/table#attributes
\> This component also supports all native \`\<table>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Table component
 */
interface TableSlots {
  expanded(): any;
  empty(): any;
  loading(): any;
  caption(): any;
  body-top(): any;
  body-bottom(): any;
}
```

### Expose

You can access the typed component instance using [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="&#x22;nofollow&#x22;"}.

```vue
<script setup lang="ts">
const table = useTemplateRef('table')
</script>

<template>
  <UTable ref="table" />
</template>
```

This will give you access to the following:

| Name                                                                                                                           | Type                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tableRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLTableElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}                                                                         |
| `tableApi`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | [`Table`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"}](https://tanstack.com/table/latest/docs/api/core/table#table-api){rel="&#x22;nofollow&#x22;"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    table: {
      slots: {
        root: 'relative overflow-auto',
        base: 'min-w-full',
        caption: 'sr-only',
        thead: 'relative',
        tbody: 'isolate [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
        tfoot: 'relative',
        tr: 'data-[selected=true]:bg-elevated/50',
        th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
        td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
        separator: 'absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)',
        empty: 'py-6 text-center text-sm text-muted',
        loading: 'py-6 text-center'
      },
      variants: {
        virtualize: {
          false: {
            base: 'overflow-clip',
            tbody: 'divide-y divide-default'
          }
        },
        pinned: {
          true: {
            th: 'sticky bg-default/75 z-1',
            td: 'sticky bg-default/75 z-1'
          }
        },
        sticky: {
          true: {
            thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur z-1',
            tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur z-1'
          },
          header: {
            thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur z-1'
          },
          footer: {
            tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur z-1'
          }
        },
        loading: {
          true: {
            thead: 'after:absolute after:z-1 after:h-px'
          }
        },
        loadingAnimation: {
          carousel: '',
          'carousel-inverse': '',
          swing: '',
          elastic: ''
        },
        loadingColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        }
      },
      compoundVariants: [
        {
          loading: true,
          loadingColor: 'primary',
          class: {
            thead: 'after:bg-primary'
          }
        },
        {
          loading: true,
          loadingColor: 'secondary',
          class: {
            thead: 'after:bg-secondary'
          }
        },
        {
          loading: true,
          loadingColor: 'success',
          class: {
            thead: 'after:bg-success'
          }
        },
        {
          loading: true,
          loadingColor: 'info',
          class: {
            thead: 'after:bg-info'
          }
        },
        {
          loading: true,
          loadingColor: 'warning',
          class: {
            thead: 'after:bg-warning'
          }
        },
        {
          loading: true,
          loadingColor: 'error',
          class: {
            thead: 'after:bg-error'
          }
        },
        {
          loading: true,
          loadingColor: 'neutral',
          class: {
            thead: 'after:bg-inverted'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel',
          class: {
            thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel-inverse',
          class: {
            thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'swing',
          class: {
            thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'elastic',
          class: {
            thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
          }
        }
      ],
      defaultVariants: {
        loadingColor: 'primary',
        loadingAnimation: 'carousel'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Table.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/table.ts).


# Tabs

## Usage

Use the Tabs component to display a list of items in a tabs.

```vue [TabsExample.vue]
<script setup lang="ts">
const items = [
  {
    label: 'Account',
    icon: 'i-lucide-user',
    slot: 'account'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    slot: 'password'
  }
]

const state = reactive({
  name: 'Benjamin Canac',
  username: 'benjamincanac',
  currentPassword: '',
  newPassword: '',
  confirmPassword: ''
})
</script>

<template>
  <UTabs :items="items">
    <template #account>
      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Name" name="name">
          <UInput v-model="state.name" class="w-full" />
        </UFormField>
        <UFormField label="Username" name="username">
          <UInput v-model="state.username" class="w-full" />
        </UFormField>
      </UForm>
    </template>

    <template #password>
      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Current Password" name="current" required>
          <UInput v-model="state.currentPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="New Password" name="new" required>
          <UInput v-model="state.newPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="Confirm Password" name="confirm" required>
          <UInput v-model="state.confirmPassword" type="password" required class="w-full" />
        </UFormField>
      </UForm>
    </template>
  </UTabs>
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `badge?: string | number | BadgeProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `content?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `value?: string | number`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, leadingAvatarSize?: ClassNameValue, label?: ClassNameValue, trailingBadge?: ClassNameValue, trailingBadgeSize?: ClassNameValue, content?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.',
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.',
  },
])
</script>

<template>
  <UTabs class="w-full" :items="items" />
</template>
```

### Content

Set the `content` prop to `false` to turn the Tabs into a toggle-only control without displaying any content. Defaults to `true`.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.',
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.',
  },
])
</script>

<template>
  <UTabs :content="false" class="w-full" :items="items" />
</template>
```

### Unmount

Use the `unmount-on-hide` prop to prevent the content from being unmounted when the Tabs is collapsed. Defaults to `true`.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.',
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.',
  },
])
</script>

<template>
  <UTabs :unmount-on-hide="false" class="w-full" :items="items" />
</template>
```

\> \[!NOTE]
\> You can inspect the DOM to see each item's content being rendered.

### Color

Use the `color` prop to change the color of the Tabs.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
  },
  {
    label: 'Password',
  },
])
</script>

<template>
  <UTabs color="neutral" :content="false" class="w-full" :items="items" />
</template>
```

### Variant

Use the `variant` prop to change the variant of the Tabs.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
  },
  {
    label: 'Password',
  },
])
</script>

<template>
  <UTabs color="neutral" variant="link" :content="false" class="w-full" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the Tabs.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
  },
  {
    label: 'Password',
  },
])
</script>

<template>
  <UTabs size="md" variant="pill" :content="false" class="w-full" :items="items" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Tabs. Defaults to `horizontal`.

```vue
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    label: 'Account',
  },
  {
    label: 'Password',
  },
])
</script>

<template>
  <UTabs orientation="vertical" variant="pill" :content="false" class="w-full" :items="items" />
</template>
```

## Examples

### Control active item

You can control the active item by using the `default-value` prop or the `v-model` directive with the `value` of the item. If no `value` is provided, it defaults to the index **as a string**.

```vue [TabsModelValueExample.vue]
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items: TabsItem[] = [
  {
    label: 'Account',
    icon: 'i-lucide-user'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock'
  }
]

const active = ref('0')

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    active.value = String((Number(active.value) + 1) % items.length)
  }, 2000)
})
</script>

<template>
  <UTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to change the key used to match items when a \`v-model\` or \`default-value\` is provided.

### With route query

You can control the active item by a URL query parameter, using `route.query.tab` as the `value` of the item.

```vue [TabsRouteQueryExample.vue]
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const route = useRoute()
const router = useRouter()

const items: TabsItem[] = [
  {
    label: 'Account',
    icon: 'i-lucide-user',
    value: 'account'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    value: 'password'
  }
]

const active = computed({
  get() {
    return (route.query.tab as string) || 'account'
  },
  set(tab) {
    // Hash is specified here to prevent the page from scrolling to the top
    router.push({
      path: '/docs/components/tabs',
      query: { tab },
      hash: '#with-route-query'
    })
  }
})
</script>

<template>
  <UTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>
```

### With content slot

Use the `#content` slot to customize the content of each item.

```vue [TabsContentSlotExample.vue]
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items: TabsItem[] = [
  {
    label: 'Account',
    icon: 'i-lucide-user'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock'
  }
]
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #content="{ item }">
      <p>This is the {{ item.label }} tab.</p>
    </template>
  </UTabs>
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [TabsCustomSlotExample.vue]
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = [
  {
    label: 'Account',
    description: 'Make changes to your account here. Click save when you\'re done.',
    icon: 'i-lucide-user',
    slot: 'account' as const
  },
  {
    label: 'Password',
    description: 'Change your password here. After saving, you\'ll be logged out.',
    icon: 'i-lucide-lock',
    slot: 'password' as const
  }
] satisfies TabsItem[]

const state = reactive({
  name: 'Benjamin Canac',
  username: 'benjamincanac',
  currentPassword: '',
  newPassword: '',
  confirmPassword: ''
})
</script>

<template>
  <UTabs :items="items" variant="link" :ui="{ trigger: 'grow' }" class="gap-4 w-full">
    <template #account="{ item }">
      <p class="text-muted mb-4">
        {{ item.description }}
      </p>

      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Name" name="name">
          <UInput v-model="state.name" class="w-full" />
        </UFormField>
        <UFormField label="Username" name="username">
          <UInput v-model="state.username" class="w-full" />
        </UFormField>

        <UButton label="Save changes" type="submit" variant="soft" class="self-end" />
      </UForm>
    </template>

    <template #password="{ item }">
      <p class="text-muted mb-4">
        {{ item.description }}
      </p>

      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Current Password" name="current" required>
          <UInput v-model="state.currentPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="New Password" name="new" required>
          <UInput v-model="state.newPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="Confirm Password" name="confirm" required>
          <UInput v-model="state.confirmPassword" type="password" required class="w-full" />
        </UFormField>

        <UButton label="Change password" type="submit" variant="soft" class="self-end" />
      </UForm>
    </template>
  </UTabs>
</template>
```

## API

### Props

```ts
/**
 * Props for the Tabs component
 */
interface TabsProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  items?: T[] | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "pill" | "link" | undefined;
  size?: "sm" | "xs" | "md" | "lg" | "xl" | undefined;
  /**
   * The orientation of the tabs.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * The content of the tabs, can be disabled to prevent rendering the content.
   * @default "true"
   */
  content?: boolean | undefined;
  /**
   * The key used to get the value from the item.
   * @default "\"value\""
   */
  valueKey?: GetItemKeys<T> | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  ui?: { root?: ClassNameValue; list?: ClassNameValue; indicator?: ClassNameValue; trigger?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; label?: ClassNameValue; trailingBadge?: ClassNameValue; trailingBadgeSize?: ClassNameValue; content?: ClassNameValue; } | undefined;
  /**
   * The value of the tab that should be active when initially rendered. Use when you do not need to control the state of the tabs
   * @default "\"0\""
   */
  defaultValue?: string | number | undefined;
  /**
   * The controlled value of the tab to activate. Can be bind as `v-model`.
   */
  modelValue?: string | number | undefined;
  /**
   * Whether a tab is activated automatically (on focus) or manually (on click).
   */
  activationMode?: "automatic" | "manual" | undefined;
  /**
   * When `true`, the element will be unmounted on closed state.
   * @default "true"
   */
  unmountOnHide?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Tabs component
 */
interface TabsSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  content(): any;
  list-leading(): any;
  list-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Tabs component
 */
interface TabsEmits {
  update:modelValue: (payload: [payload: string | number]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                                 |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggersRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    tabs: {
      slots: {
        root: 'flex items-center gap-2',
        list: 'relative flex p-1 group',
        indicator: 'absolute transition-[translate,width] duration-200',
        trigger: [
          'group relative inline-flex items-center min-w-0 data-[state=inactive]:text-muted hover:data-[state=inactive]:not-disabled:text-default font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        label: 'truncate',
        trailingBadge: 'shrink-0',
        trailingBadgeSize: 'sm',
        content: 'focus:outline-none w-full'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          pill: {
            list: 'bg-elevated rounded-lg',
            trigger: 'grow',
            indicator: 'rounded-md shadow-xs'
          },
          link: {
            list: 'border-default',
            indicator: 'rounded-full',
            trigger: 'focus:outline-none'
          }
        },
        orientation: {
          horizontal: {
            root: 'flex-col',
            list: 'w-full',
            indicator: 'left-0 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position)',
            trigger: 'justify-center'
          },
          vertical: {
            list: 'flex-col',
            indicator: 'top-0 h-(--reka-tabs-indicator-size) translate-y-(--reka-tabs-indicator-position)'
          }
        },
        size: {
          xs: {
            trigger: 'px-2 py-1 text-xs gap-1',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs'
          },
          sm: {
            trigger: 'px-2.5 py-1.5 text-xs gap-1.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs'
          },
          md: {
            trigger: 'px-3 py-1.5 text-sm gap-1.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs'
          },
          lg: {
            trigger: 'px-3 py-2 text-sm gap-2',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs'
          },
          xl: {
            trigger: 'px-3 py-2 text-base gap-2',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          variant: 'pill',
          class: {
            indicator: 'inset-y-1'
          }
        },
        {
          orientation: 'horizontal',
          variant: 'link',
          class: {
            list: 'border-b -mb-px',
            indicator: '-bottom-px h-px'
          }
        },
        {
          orientation: 'vertical',
          variant: 'pill',
          class: {
            indicator: 'inset-x-1',
            list: 'items-center'
          }
        },
        {
          orientation: 'vertical',
          variant: 'link',
          class: {
            list: 'border-s -ms-px',
            indicator: '-start-px w-px'
          }
        },
        {
          color: 'primary',
          variant: 'pill',
          class: {
            indicator: 'bg-primary',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'pill',
          class: {
            indicator: 'bg-secondary',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary'
          }
        },
        {
          color: 'success',
          variant: 'pill',
          class: {
            indicator: 'bg-success',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success'
          }
        },
        {
          color: 'info',
          variant: 'pill',
          class: {
            indicator: 'bg-info',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info'
          }
        },
        {
          color: 'warning',
          variant: 'pill',
          class: {
            indicator: 'bg-warning',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-warning'
          }
        },
        {
          color: 'error',
          variant: 'pill',
          class: {
            indicator: 'bg-error',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-error'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          class: {
            indicator: 'bg-inverted',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
          }
        },
        {
          color: 'primary',
          variant: 'link',
          class: {
            indicator: 'bg-primary',
            trigger: 'data-[state=active]:text-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
          }
        },
        {
          color: 'secondary',
          variant: 'link',
          class: {
            indicator: 'bg-secondary',
            trigger: 'data-[state=active]:text-secondary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
          }
        },
        {
          color: 'success',
          variant: 'link',
          class: {
            indicator: 'bg-success',
            trigger: 'data-[state=active]:text-success focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
          }
        },
        {
          color: 'info',
          variant: 'link',
          class: {
            indicator: 'bg-info',
            trigger: 'data-[state=active]:text-info focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
          }
        },
        {
          color: 'warning',
          variant: 'link',
          class: {
            indicator: 'bg-warning',
            trigger: 'data-[state=active]:text-warning focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
          }
        },
        {
          color: 'error',
          variant: 'link',
          class: {
            indicator: 'bg-error',
            trigger: 'data-[state=active]:text-error focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          class: {
            indicator: 'bg-inverted',
            trigger: 'data-[state=active]:text-highlighted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'pill',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Tabs.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/tabs.ts).


# Textarea

## Usage

Use the `v-model` directive to control the value of the Textarea.

```vue
<template>
  <UTextarea model-value="" />
</template>
```

### Rows

Use the `rows` prop to set the number of rows. Defaults to `3`.

```vue
<template>
  <UTextarea :rows="12" />
</template>
```

### Placeholder

Use the `placeholder` prop to set a placeholder text.

```vue
<template>
  <UTextarea placeholder="Type something..." />
</template>
```

### Autoresize

Use the `autoresize` prop to enable autoresizing the height of the Textarea.

```vue
<template>
  <UTextarea model-value="This is a long text that will autoresize the height of the Textarea." autoresize />
</template>
```

Use the `maxrows` prop to set the maximum number of rows when autoresizing. If set to `0`, the Textarea will grow indefinitely.

```vue
<template>
  <UTextarea model-value="This is a long text that will autoresize the height of the Textarea with a maximum of 4 rows." :maxrows="4" autoresize />
</template>
```

### Color

Use the `color` prop to change the ring color when the Textarea is focused.

```vue
<template>
  <UTextarea color="neutral" highlight placeholder="Type something..." />
</template>
```

\> \[!NOTE]
\> The \`highlight\` prop is used here to show the focus state. It's used internally when a validation error occurs.

### Variant

Use the `variant` prop to change the variant of the Textarea.

```vue
<template>
  <UTextarea color="neutral" variant="subtle" :highlight="false" placeholder="Type something..." />
</template>
```

### Size

Use the `size` prop to change the size of the Textarea.

```vue
<template>
  <UTextarea size="xl" placeholder="Type something..." />
</template>
```

### Icon

Use the `icon` prop to show an [Icon](https://ui.nuxt.com/docs/components/icon) inside the Textarea.

```vue
<template>
  <UTextarea icon="i-lucide-search" size="md" variant="outline" placeholder="Search..." :rows="1" />
</template>
```

Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.

```vue
<template>
  <UTextarea trailing-icon="i-lucide-at-sign" placeholder="Enter your email" size="md" :rows="1" />
</template>
```

### Avatar

Use the `avatar` prop to show an [Avatar](https://ui.nuxt.com/docs/components/avatar) inside the Textarea.

```vue
<template>
  <UTextarea size="md" variant="outline" placeholder="Search..." :rows="1" />
</template>
```

### Loading

Use the `loading` prop to show a loading icon on the Textarea.

```vue
<template>
  <UTextarea loading :trailing="false" placeholder="Search..." :rows="1" />
</template>
```

### Loading Icon

Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

```vue
<template>
  <UTextarea loading loading-icon="i-lucide-loader" placeholder="Search..." :rows="1" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.loading\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.loading\` key.

### Disabled

Use the `disabled` prop to disable the Textarea.

```vue
<template>
  <UTextarea disabled placeholder="Type something..." />
</template>
```

## API

### Props

```ts
/**
 * Props for the Textarea component
 */
interface TextareaProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  id?: string | undefined;
  name?: string | undefined;
  /**
   * The placeholder text when the textarea is empty.
   */
  placeholder?: string | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  variant?: "outline" | "soft" | "subtle" | "ghost" | "none" | undefined;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  required?: boolean | undefined;
  autofocus?: boolean | undefined;
  /**
   * @default "0"
   */
  autofocusDelay?: number | undefined;
  autoresize?: boolean | undefined;
  /**
   * @default "0"
   */
  autoresizeDelay?: number | undefined;
  disabled?: boolean | undefined;
  /**
   * @default "3"
   */
  rows?: number | undefined;
  /**
   * @default "0"
   */
  maxrows?: number | undefined;
  /**
   * Highlight the ring color like a focus state.
   */
  highlight?: boolean | undefined;
  /**
   * Keep the mobile text size on all breakpoints.
   */
  fixed?: boolean | undefined;
  defaultValue?: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod> | undefined;
  modelValue?: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod> | undefined;
  modelModifiers?: Mod | undefined;
  ui?: { root?: ClassNameValue; base?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; } | undefined;
  /**
   * Display an icon based on the `leading` and `trailing` props.
   */
  icon?: any;
  /**
   * Display an avatar on the left side.
   */
  avatar?: AvatarProps | undefined;
  /**
   * When `true`, the icon will be displayed on the left side.
   */
  leading?: boolean | undefined;
  /**
   * Display an icon on the left side.
   */
  leadingIcon?: any;
  /**
   * When `true`, the icon will be displayed on the right side.
   */
  trailing?: boolean | undefined;
  /**
   * Display an icon on the right side.
   */
  trailingIcon?: any;
  /**
   * When `true`, the loading icon will be displayed.
   */
  loading?: boolean | undefined;
  /**
   * The icon when the `loading` prop is `true`.
   */
  loadingIcon?: any;
  autocomplete?: string | undefined;
  cols?: Numberish | undefined;
  dirname?: string | undefined;
  form?: string | undefined;
  maxlength?: Numberish | undefined;
  minlength?: Numberish | undefined;
  readonly?: Booleanish | undefined;
  wrap?: string | undefined;
}
```

\> \[!NOTE]
\> See: https\://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attributes
\> This component also supports all native \`\<textarea>\` HTML attributes.

### Slots

```ts
/**
 * Slots for the Textarea component
 */
interface TextareaSlots {
  leading(): any;
  default(): any;
  trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Textarea component
 */
interface TextareaEmits {
  update:modelValue: (payload: [value: _Number<_Optional<_Nullable<T, Mod>, Mod>, Mod>]) => void;
  blur: (payload: [event: FocusEvent]) => void;
  change: (payload: [event: Event]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                              | Type                                                                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `textareaRef`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<HTMLTextAreaElement | null>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    textarea: {
      slots: {
        root: 'relative inline-flex items-center',
        base: [
          'w-full rounded-md border-0 appearance-none placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leading: 'absolute start-0 flex items-start',
        leadingIcon: 'shrink-0 text-dimmed',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        trailing: 'absolute end-0 flex items-start',
        trailingIcon: 'shrink-0 text-dimmed'
      },
      variants: {
        fieldGroup: {
          horizontal: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none'
          },
          vertical: {
            root: 'group has-focus-visible:z-[1]',
            base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none'
          }
        },
        size: {
          xs: {
            base: 'px-2 py-1 text-sm/4 gap-1',
            leading: 'ps-2 inset-y-1',
            trailing: 'pe-2 inset-y-1',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          sm: {
            base: 'px-2.5 py-1.5 text-sm/4 gap-1.5',
            leading: 'ps-2.5 inset-y-1.5',
            trailing: 'pe-2.5 inset-y-1.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs',
            trailingIcon: 'size-4'
          },
          md: {
            base: 'px-2.5 py-1.5 text-base/5 gap-1.5',
            leading: 'ps-2.5 inset-y-1.5',
            trailing: 'pe-2.5 inset-y-1.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          lg: {
            base: 'px-3 py-2 text-base/5 gap-2',
            leading: 'ps-3 inset-y-2',
            trailing: 'pe-3 inset-y-2',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs',
            trailingIcon: 'size-5'
          },
          xl: {
            base: 'px-3 py-2 text-base gap-2',
            leading: 'ps-3 inset-y-2',
            trailing: 'pe-3 inset-y-2',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs',
            trailingIcon: 'size-6'
          }
        },
        variant: {
          outline: 'text-highlighted bg-default ring ring-inset ring-accented',
          soft: 'text-highlighted bg-elevated/50 hover:bg-elevated focus:bg-elevated disabled:bg-elevated/50',
          subtle: 'text-highlighted bg-elevated ring ring-inset ring-accented',
          ghost: 'text-highlighted bg-transparent hover:bg-elevated focus:bg-elevated disabled:bg-transparent dark:disabled:bg-transparent',
          none: 'text-highlighted bg-transparent'
        },
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        leading: {
          true: ''
        },
        trailing: {
          true: ''
        },
        loading: {
          true: ''
        },
        highlight: {
          true: ''
        },
        fixed: {
          false: ''
        },
        type: {
          file: 'file:me-1.5 file:font-medium file:text-muted file:outline-none'
        },
        autoresize: {
          true: {
            base: 'resize-none'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
        },
        {
          color: 'secondary',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary'
        },
        {
          color: 'success',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success'
        },
        {
          color: 'info',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info'
        },
        {
          color: 'warning',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning'
        },
        {
          color: 'error',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'ring ring-inset ring-primary'
        },
        {
          color: 'secondary',
          highlight: true,
          class: 'ring ring-inset ring-secondary'
        },
        {
          color: 'success',
          highlight: true,
          class: 'ring ring-inset ring-success'
        },
        {
          color: 'info',
          highlight: true,
          class: 'ring ring-inset ring-info'
        },
        {
          color: 'warning',
          highlight: true,
          class: 'ring ring-inset ring-warning'
        },
        {
          color: 'error',
          highlight: true,
          class: 'ring ring-inset ring-error'
        },
        {
          color: 'neutral',
          variant: [
            'outline',
            'subtle'
          ],
          class: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'ring ring-inset ring-inverted'
        },
        {
          leading: true,
          size: 'xs',
          class: 'ps-7'
        },
        {
          leading: true,
          size: 'sm',
          class: 'ps-8'
        },
        {
          leading: true,
          size: 'md',
          class: 'ps-9'
        },
        {
          leading: true,
          size: 'lg',
          class: 'ps-10'
        },
        {
          leading: true,
          size: 'xl',
          class: 'ps-11'
        },
        {
          trailing: true,
          size: 'xs',
          class: 'pe-7'
        },
        {
          trailing: true,
          size: 'sm',
          class: 'pe-8'
        },
        {
          trailing: true,
          size: 'md',
          class: 'pe-9'
        },
        {
          trailing: true,
          size: 'lg',
          class: 'pe-10'
        },
        {
          trailing: true,
          size: 'xl',
          class: 'pe-11'
        },
        {
          loading: true,
          leading: true,
          class: {
            leadingIcon: 'animate-spin'
          }
        },
        {
          loading: true,
          leading: false,
          trailing: true,
          class: {
            trailingIcon: 'animate-spin'
          }
        },
        {
          fixed: false,
          size: 'xs',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'sm',
          class: 'md:text-xs'
        },
        {
          fixed: false,
          size: 'md',
          class: 'md:text-sm'
        },
        {
          fixed: false,
          size: 'lg',
          class: 'md:text-sm'
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'outline'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Textarea.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/textarea.ts).


# Theme

## Usage

The Theme component allows you to override the theme of all child components without modifying each one individually. It uses Vue's `provide` / `inject` mechanism under the hood, so the overrides apply at any depth.

Use the `ui` prop to pass an object where keys are component names (camelCase) and values are their slot class overrides:

```vue [ThemeExample.vue]
<template>
  <UTheme
    :ui="{
      button: {
        base: 'rounded-full'
      }
    }"
  >
    <div class="flex items-center gap-2">
      <UButton label="Button" color="neutral" />
      <UButton label="Button" color="neutral" variant="outline" />
      <UButton label="Button" color="neutral" variant="subtle" />
    </div>
  </UTheme>
</template>
```

\> \[!NOTE]
\> The Theme component doesn't render any HTML element, it only provides theme overrides to its children.

\*\*Nuxt:\*\*
\> \[!TIP]
\> For app-level theme configuration, we recommend using the \`app.config.ts\` file instead.
\*\*Vue:\*\*
\> \[!TIP]
\> For app-level theme configuration, we recommend using the \`vite.config.ts\` file instead.

### Multiple

You can theme multiple component types at once by passing different keys in the `ui` prop.

```vue [ThemeMultipleExample.vue]
<template>
  <UTheme
    :ui="{
      button: {
        base: 'rounded-full'
      },
      input: {
        base: 'rounded-full'
      },
      select: {
        base: 'rounded-full'
      }
    }"
  >
    <div class="flex items-center gap-2">
      <UButton label="Button" color="neutral" variant="outline" />
      <UInput placeholder="Search..." />
      <USelect placeholder="Select" :items="['Item 1', 'Item 2', 'Item 3']" />
    </div>
  </UTheme>
</template>
```

### Nested

Theme components can be nested. When nested, the innermost Theme's overrides take precedence for the components it wraps.

```vue [ThemeNestedExample.vue]
<template>
  <UTheme
    :ui="{
      button: {
        base: 'rounded-full'
      }
    }"
  >
    <div class="flex flex-col items-start gap-4 border border-muted p-4 rounded-lg">
      <div class="flex items-center gap-2">
        <UButton label="Outer theme" />
        <UButton label="Outer theme" color="neutral" variant="outline" />
      </div>

      <UTheme
        :ui="{
          button: {
            base: 'font-black uppercase'
          }
        }"
      >
        <div class="border border-muted p-4 rounded-lg">
          <div class="flex items-center gap-2">
            <UButton label="Inner theme" />
            <UButton label="Inner theme" color="neutral" variant="outline" />
          </div>
        </div>
      </UTheme>
    </div>
  </UTheme>
</template>
```

### Priority

The `ui` prop on individual components always takes priority over the Theme component. This lets you override specific instances while still benefiting from the shared theme.

```vue [ThemePriorityExample.vue]
<template>
  <UTheme
    :ui="{
      button: {
        base: 'rounded-full'
      }
    }"
  >
    <div class="flex items-center gap-2">
      <UButton label="Themed" />
      <UButton label="Overridden" :ui="{ base: 'rounded-none' }" />
    </div>
  </UTheme>
</template>
```

### Deep

Because the Theme component uses Vue's `provide` / `inject`, the overrides are available to all descendant components regardless of how deeply nested they are.

```vue [ThemeDeepExample.vue]
<script setup lang="ts">
import MyButton from './MyButton.vue'
</script>

<template>
  <UTheme
    :ui="{
      button: {
        base: 'rounded-full'
      }
    }"
  >
    <UCard :ui="{ body: 'flex items-center gap-2 sm:flex-row flex-col' }">
      <UButton label="Direct child" />
      <MyButton />
    </UCard>
  </UTheme>
</template>
```

\> \[!NOTE]
\> In this example, \`MyButton\` is a custom component that renders a \`UButton\` internally. The theme overrides still apply because they propagate through the entire component tree.

## Examples

### With form components

Use the Theme component to apply consistent styling across a group of form components.

```vue [ThemeFormExample.vue]
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  name: z.string().min(2, 'Too short'),
  email: z.email('Invalid email'),
  bio: z.string().optional()
})

type Schema = z.output<typeof schema>

const state = reactive<Partial<Schema>>({
  name: 'John Doe',
  email: 'john@example.com',
  bio: undefined
})

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Saved', description: 'Your profile has been updated.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UTheme
    :ui="{
      formField: {
        root: 'flex max-sm:flex-col justify-between gap-4',
        wrapper: 'w-full sm:max-w-xs'
      }
    }"
  >
    <UForm :schema="schema" :state="state" class="space-y-4 w-full" @submit="onSubmit">
      <UFormField label="Name" name="name" description="Your public display name.">
        <UInput v-model="state.name" />
      </UFormField>

      <UFormField label="Email" name="email" description="Used for notifications.">
        <UInput v-model="state.email" type="email" />
      </UFormField>

      <UFormField label="Bio" name="bio" description="A short description about yourself.">
        <UTextarea v-model="state.bio" placeholder="Tell us about yourself" />
      </UFormField>

      <div class="flex justify-end">
        <UButton type="submit">
          Save changes
        </UButton>
      </div>
    </UForm>
  </UTheme>
</template>
```

### With prose components

You can theme prose (typography) components by nesting them under the `prose` key. This is useful when rendering Markdown content with a tighter or custom typographic scale.

```vue [ThemeProseExample.vue]
<script setup lang="ts">
const ui = {
  prose: {
    p: { base: 'my-2.5 text-sm/6' },
    li: { base: 'my-0.5 text-sm/6' },
    ul: { base: 'my-2.5' },
    ol: { base: 'my-2.5' },
    h1: { base: 'text-xl mb-4' },
    h2: { base: 'text-lg mt-6 mb-3' },
    h3: { base: 'text-base mt-4 mb-2' },
    h4: { base: 'text-sm mt-3 mb-1.5' },
    code: { base: 'text-xs' },
    pre: { root: 'my-2.5', base: 'text-xs/5' },
    table: { root: 'my-2.5' },
    hr: { base: 'my-5' }
  }
}

const value = `## Getting started

This is a paragraph with a **tighter typographic scale** applied through the \`Theme\` component.

- First item
- Second item
- Third item

> The spacing and font sizes are smaller than the defaults, making it ideal for compact content areas.`
</script>

<template>
  <UTheme :ui="ui">
    <MDC :value="value" />
  </UTheme>
</template>
```

## API

### Props

```ts
/**
 * Props for the Theme component
 */
interface ThemeProps {
  ui: object;
}
```

### Slots

```ts
/**
 * Slots for the Theme component
 */
interface ThemeSlots {
  default(): any;
}
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Theme.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/theme.ts).


# Timeline

## Usage

Use the Timeline component to display a list of items in a timeline.

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization. Deployed the application to production.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `date?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `title?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `description?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `avatar?: AvatarProps`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `value?: string | number`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- [`slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}](https://ui.nuxt.com/#with-custom-slot)
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, indicator?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization. Deployed the application to production.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline :default-value="2" class="w-96" :items="items" />
</template>
```

### Color

Use the `color` prop to change the color of the active items in a Timeline.

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization. Deployed the application to production.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline color="neutral" :default-value="2" class="w-96" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the Timeline.

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization. Deployed the application to production.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline size="xs" :default-value="2" class="w-96" :items="items" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation of the Timeline. Defaults to `vertical`.

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline orientation="horizontal" :default-value="2" class="w-full" :items="items" />
</template>
```

### Reverse

Use the reverse prop to reverse the direction of the Timeline.

```vue
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = ref<TimelineItem[]>([
  {
    date: 'Mar 15, 2025',
    title: 'Project Kickoff',
    description: 'Kicked off the project with team alignment.',
    icon: 'i-lucide-rocket',
  },
  {
    date: 'Mar 22 2025',
    title: 'Design Phase',
    description: 'User research and design workshops.',
    icon: 'i-lucide-palette',
  },
  {
    date: 'Mar 29 2025',
    title: 'Development Sprint',
    description: 'Frontend and backend development.',
    icon: 'i-lucide-code',
  },
  {
    date: 'Apr 5 2025',
    title: 'Testing & Deployment',
    description: 'QA testing and performance optimization.',
    icon: 'i-lucide-check-circle',
  },
])
</script>

<template>
  <UTimeline reverse :model-value="2" orientation="vertical" class="w-full" :items="items" />
</template>
```

## Examples

### Control active item

You can control the active item by using the `default-value` prop or the `v-model` directive with the `value` of the item. If no `value` is provided, it defaults to the index.

```vue [TimelineModelValueExample.vue]
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items: TimelineItem[] = [{
  date: 'Mar 15, 2025',
  title: 'Project Kickoff',
  description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
  icon: 'i-lucide-rocket',
  value: 'kickoff'
}, {
  date: 'Mar 22, 2025',
  title: 'Design Phase',
  description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
  icon: 'i-lucide-palette',
  value: 'design'
}, {
  date: 'Mar 29, 2025',
  title: 'Development Sprint',
  description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
  icon: 'i-lucide-code',
  value: 'development'
}, {
  date: 'Apr 5, 2025',
  title: 'Testing & Deployment',
  description: 'QA testing and performance optimization. Deployed the application to production.',
  icon: 'i-lucide-check-circle',
  value: 'deployment'
}]

const active = ref(0)

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    active.value = (active.value + 1) % items.length
  }, 2000)
})
</script>

<template>
  <UTimeline v-model="active" :items="items" class="w-96" />
</template>
```

\> \[!TIP]
\> Use the \`value-key\` prop to change the key used to match items when a \`v-model\` or \`default-value\` is provided.

### With select event

You can add a `@select` listener to make items clickable.

\> \[!NOTE]
\> The handler function receives the \`Event\` and \`TimelineItem\` as the first and second arguments respectively.

```vue [TimelineSelectExample.vue]
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items: TimelineItem[] = [{
  date: 'Mar 15, 2025',
  title: 'Project Kickoff',
  description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
  icon: 'i-lucide-rocket',
  value: 'kickoff'
}, {
  date: 'Mar 22, 2025',
  title: 'Design Phase',
  description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
  icon: 'i-lucide-palette',
  value: 'design'
}, {
  date: 'Mar 29, 2025',
  title: 'Development Sprint',
  description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
  icon: 'i-lucide-code',
  value: 'development'
}, {
  date: 'Apr 5, 2025',
  title: 'Testing & Deployment',
  description: 'QA testing and performance optimization. Deployed the application to production.',
  icon: 'i-lucide-check-circle',
  value: 'deployment'
}]

const active = ref<string | number>('kickoff')

function onSelect(_e: Event, item: TimelineItem) {
  if (item.value) {
    active.value = item.value
  }
}
</script>

<template>
  <UTimeline
    v-model="active"
    :items="items"
    class="w-96"
    @select="onSelect"
  />
</template>
```

### With alternating layout

Use the `ui` prop to create a Timeline with alternating layout.

```vue [TimelineAlternatingLayoutExample.vue]
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items: TimelineItem[] = [{
  date: 'Mar 15, 2025',
  title: 'Project Kickoff',
  icon: 'i-lucide-rocket',
  value: 'kickoff'
}, {
  date: 'Mar 22, 2025',
  title: 'Design Phase',
  icon: 'i-lucide-palette',
  value: 'design'
}, {
  date: 'Mar 29, 2025',
  title: 'Development Sprint',
  icon: 'i-lucide-code',
  value: 'development'
}, {
  date: 'Apr 5, 2025',
  title: 'Testing & Deployment',
  icon: 'i-lucide-check-circle',
  value: 'deployment'
}]
</script>

<template>
  <UTimeline
    :items="items"
    :default-value="2"
    :ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
    class="translate-x-[calc(50%-1rem)]"
  />
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}-indicator`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-date`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-title`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-description`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [TimelineCustomSlotExample.vue]
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'

const items = [{
  date: 'Mar 15, 2025',
  title: 'Project Kickoff',
  subtitle: 'Project Initiation',
  description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
  icon: 'i-lucide-rocket',
  value: 'kickoff'
}, {
  date: 'Mar 22, 2025',
  title: 'Design Phase',
  description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
  icon: 'i-lucide-palette',
  value: 'design'
}, {
  date: 'Mar 29, 2025',
  title: 'Development Sprint',
  description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
  icon: 'i-lucide-code',
  value: 'development',
  slot: 'development' as const,
  developers: [
    {
      src: 'https://github.com/J-Michalek.png',
      loading: 'lazy' as const
    }, {
      src: 'https://github.com/benjamincanac.png',
      loading: 'lazy' as const
    }
  ]
}, {
  date: 'Apr 5, 2025',
  title: 'Testing & Deployment',
  description: 'QA testing and performance optimization. Deployed the application to production.',
  icon: 'i-lucide-check-circle',
  value: 'deployment'
}] satisfies TimelineItem[]
</script>

<template>
  <UTimeline :items="items" :default-value="2" class="w-96">
    <template #development-title="{ item }">
      <div class="flex items-center gap-1">
        <span>{{ item.title }}</span>

        <UAvatarGroup size="2xs">
          <UAvatar v-for="(developer, index) of item.developers" :key="index" v-bind="developer" />
        </UAvatarGroup>
      </div>
    </template>
  </UTimeline>
</template>
```

### With slots

Use the available slots to create a more complex Timeline.

```vue [TimelineSlotsExample.vue]
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import { useTimeAgo } from '@vueuse/core'

const items = [{
  username: 'J-Michalek',
  date: '2025-05-24T14:58:55Z',
  action: 'opened this',
  avatar: {
    src: 'https://github.com/J-Michalek.png',
    loading: 'lazy' as const
  }
}, {
  username: 'J-Michalek',
  date: '2025-05-26T19:30:14+02:00',
  action: 'marked this pull request as ready for review',
  icon: 'i-lucide-check-circle'
}, {
  username: 'benjamincanac',
  date: '2025-05-27T11:01:20Z',
  action: 'commented on this',
  description: 'I\'ve made a few changes, let me know what you think! Basically I updated the design, removed unnecessary divs, used Avatar component for the indicator since it supports icon already.',
  avatar: {
    src: 'https://github.com/benjamincanac.png',
    loading: 'lazy' as const
  }
}, {
  username: 'J-Michalek',
  date: '2025-05-27T11:01:20Z',
  action: 'commented on this',
  description: 'Looks great! Good job on cleaning it up.',
  avatar: {
    src: 'https://github.com/J-Michalek.png',
    loading: 'lazy' as const
  }
}, {
  username: 'benjamincanac',
  date: '2025-05-27T11:01:20Z',
  action: 'merged this',
  icon: 'i-lucide-git-merge'
}] satisfies TimelineItem[]
</script>

<template>
  <UTimeline
    :items="items"
    size="xs"
    :ui="{
      date: 'float-end ms-1',
      description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
    }"
    class="w-96"
  >
    <template #title="{ item }">
      <span>{{ item.username }}</span>
      <span class="font-normal text-muted">&nbsp;{{ item.action }}</span>
    </template>

    <template #date="{ item }">
      {{ useTimeAgo(new Date(item.date)) }}
    </template>
  </UTimeline>
</template>
```

## API

### Props

```ts
/**
 * Props for the Timeline component
 */
interface TimelineProps {
  items: T[];
  /**
   * The element or component this component should render as.
   */
  as?: any;
  size?: "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | undefined;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  /**
   * The orientation of the Timeline.
   * @default "\"vertical\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * The key used to get the value from the item.
   * @default "\"value\""
   */
  valueKey?: GetItemKeys<T> | undefined;
  defaultValue?: string | number | undefined;
  reverse?: boolean | undefined;
  ui?: { root?: ClassNameValue; item?: ClassNameValue; container?: ClassNameValue; indicator?: ClassNameValue; separator?: ClassNameValue; wrapper?: ClassNameValue; date?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; } | undefined;
  modelValue?: string | number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Timeline component
 */
interface TimelineSlots {
  indicator(): any;
  wrapper(): any;
  date(): any;
  title(): any;
  description(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Timeline component
 */
interface TimelineEmits {
  select: (payload: [event: Event, item: T]) => void;
  update:modelValue: (payload: [value: string | number | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    timeline: {
      slots: {
        root: 'flex gap-1.5',
        item: 'group relative flex flex-1 gap-3',
        container: 'relative flex items-center gap-1.5',
        indicator: 'group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted text-muted',
        separator: 'flex-1 rounded-full bg-elevated',
        wrapper: 'w-full',
        date: 'text-dimmed text-xs/5',
        title: 'font-medium text-highlighted text-sm',
        description: 'text-muted text-wrap text-sm'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'flex-row w-full',
            item: 'flex-col',
            separator: 'h-0.5'
          },
          vertical: {
            root: 'flex-col',
            container: 'flex-col',
            separator: 'w-0.5'
          }
        },
        color: {
          primary: {
            indicator: 'group-data-[state=completed]:bg-primary group-data-[state=active]:bg-primary'
          },
          secondary: {
            indicator: 'group-data-[state=completed]:bg-secondary group-data-[state=active]:bg-secondary'
          },
          success: {
            indicator: 'group-data-[state=completed]:bg-success group-data-[state=active]:bg-success'
          },
          info: {
            indicator: 'group-data-[state=completed]:bg-info group-data-[state=active]:bg-info'
          },
          warning: {
            indicator: 'group-data-[state=completed]:bg-warning group-data-[state=active]:bg-warning'
          },
          error: {
            indicator: 'group-data-[state=completed]:bg-error group-data-[state=active]:bg-error'
          },
          neutral: {
            indicator: 'group-data-[state=completed]:bg-inverted group-data-[state=active]:bg-inverted'
          }
        },
        size: {
          '3xs': '',
          '2xs': '',
          xs: '',
          sm: '',
          md: '',
          lg: '',
          xl: '',
          '2xl': '',
          '3xl': ''
        },
        reverse: {
          true: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-primary'
          }
        },
        {
          color: 'secondary',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-secondary'
          }
        },
        {
          color: 'success',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-success'
          }
        },
        {
          color: 'info',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-info'
          }
        },
        {
          color: 'warning',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-warning'
          }
        },
        {
          color: 'error',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-error'
          }
        },
        {
          color: 'primary',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-primary group-data-[state=completed]:bg-primary'
          }
        },
        {
          color: 'secondary',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-secondary group-data-[state=completed]:bg-secondary'
          }
        },
        {
          color: 'success',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-success group-data-[state=completed]:bg-success'
          }
        },
        {
          color: 'info',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-info group-data-[state=completed]:bg-info'
          }
        },
        {
          color: 'warning',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-warning group-data-[state=completed]:bg-warning'
          }
        },
        {
          color: 'error',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-error group-data-[state=completed]:bg-error'
          }
        },
        {
          color: 'neutral',
          reverse: false,
          class: {
            separator: 'group-data-[state=completed]:bg-inverted'
          }
        },
        {
          color: 'neutral',
          reverse: true,
          class: {
            separator: 'group-data-[state=active]:bg-inverted group-data-[state=completed]:bg-inverted'
          }
        },
        {
          orientation: 'horizontal',
          size: '3xs',
          class: {
            wrapper: 'pe-4.5'
          }
        },
        {
          orientation: 'horizontal',
          size: '2xs',
          class: {
            wrapper: 'pe-5'
          }
        },
        {
          orientation: 'horizontal',
          size: 'xs',
          class: {
            wrapper: 'pe-5.5'
          }
        },
        {
          orientation: 'horizontal',
          size: 'sm',
          class: {
            wrapper: 'pe-6'
          }
        },
        {
          orientation: 'horizontal',
          size: 'md',
          class: {
            wrapper: 'pe-6.5'
          }
        },
        {
          orientation: 'horizontal',
          size: 'lg',
          class: {
            wrapper: 'pe-7'
          }
        },
        {
          orientation: 'horizontal',
          size: 'xl',
          class: {
            wrapper: 'pe-7.5'
          }
        },
        {
          orientation: 'horizontal',
          size: '2xl',
          class: {
            wrapper: 'pe-8'
          }
        },
        {
          orientation: 'horizontal',
          size: '3xl',
          class: {
            wrapper: 'pe-8.5'
          }
        },
        {
          orientation: 'vertical',
          size: '3xs',
          class: {
            wrapper: '-mt-0.5 pb-4.5'
          }
        },
        {
          orientation: 'vertical',
          size: '2xs',
          class: {
            wrapper: 'pb-5'
          }
        },
        {
          orientation: 'vertical',
          size: 'xs',
          class: {
            wrapper: 'mt-0.5 pb-5.5'
          }
        },
        {
          orientation: 'vertical',
          size: 'sm',
          class: {
            wrapper: 'mt-1 pb-6'
          }
        },
        {
          orientation: 'vertical',
          size: 'md',
          class: {
            wrapper: 'mt-1.5 pb-6.5'
          }
        },
        {
          orientation: 'vertical',
          size: 'lg',
          class: {
            wrapper: 'mt-2 pb-7'
          }
        },
        {
          orientation: 'vertical',
          size: 'xl',
          class: {
            wrapper: 'mt-2.5 pb-7.5'
          }
        },
        {
          orientation: 'vertical',
          size: '2xl',
          class: {
            wrapper: 'mt-3 pb-8'
          }
        },
        {
          orientation: 'vertical',
          size: '3xl',
          class: {
            wrapper: 'mt-3.5 pb-8.5'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Timeline.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/timeline.ts).


# Toast

## Usage

Use the [useToast](https://ui.nuxt.com/docs/composables/use-toast) composable to display a toast in your application.

```vue [ToastExample.vue]
<script setup lang="ts">
const toast = useToast()

function addToCalendar() {
  const eventDate = new Date(Date.now() + Math.random() * 31536000000)
  const formattedDate = eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })

  toast.add({
    title: 'Event added to calendar',
    description: `This event is scheduled for ${formattedDate}.`,
    icon: 'i-lucide-calendar-days'
  })
}
</script>

<template>
  <UButton label="Add to calendar" color="neutral" variant="outline" icon="i-lucide-plus" @click="addToCalendar" />
</template>
```

\> \[!WARNING]
\> Make sure to wrap your app with the \[\`App\`]\(/docs/components/app) component which uses our \[\`Toaster\`]\(https\://github.com/nuxt/ui/blob/v4/src/runtime/components/Toaster.vue) component which uses the \[\`ToastProvider\`]\(https\://reka-ui.com/docs/components/toast#provider) component from Reka UI.

\> \[!TIP]
\> See: /docs/components/app#props
\> You can check the \`App\` component \`toaster\` prop to see how to configure the Toaster globally.

### Title

Pass a `title` field to the `toast.add` method to display a title.

```vue [ToastTitleExample.vue]
<script setup lang="ts">
const props = defineProps<{
  title: string
}>()

const toast = useToast()

function showToast() {
  toast.add(props)
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Description

Pass a `description` field to the `toast.add` method to display a description.

```vue [ToastDescriptionExample.vue]
<script setup lang="ts">
const props = defineProps<{
  title: string
  description: string
}>()

const toast = useToast()

function showToast() {
  toast.add(props)
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Icon

Pass an `icon` field to the `toast.add` method to display an [Icon](https://ui.nuxt.com/docs/components/icon).

```vue [ToastIconExample.vue]
<script setup lang="ts">
const props = defineProps<{
  icon: string
}>()

const toast = useToast()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: 'There was a problem with your request.',
    icon: props.icon
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Avatar

Pass an `avatar` field to the `toast.add` method to display an [Avatar](https://ui.nuxt.com/docs/components/avatar).

```vue [ToastAvatarExample.vue]
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'

const props = defineProps<{
  avatar: AvatarProps
}>()

const toast = useToast()

function showToast() {
  toast.add({
    title: 'User invited',
    description: 'benjamincanac was invited to the team.',
    avatar: props.avatar
  })
}
</script>

<template>
  <UButton label="Invite user" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Color

Pass a `color` field to the `toast.add` method to change the color of the Toast.

```vue [ToastColorExample.vue]
<script setup lang="ts">
import type { ToastProps } from '@nuxt/ui'

const props = defineProps<{
  color: ToastProps['color']
}>()

const toast = useToast()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: 'There was a problem with your request.',
    icon: 'i-lucide-wifi',
    color: props.color
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Close

Pass a `close` field to customize or hide the close [Button](https://ui.nuxt.com/docs/components/button) (with `false` value).

```vue [ToastCloseExample.vue]
<script setup lang="ts">
const toast = useToast()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: 'There was a problem with your request.',
    icon: 'i-lucide-wifi',
    close: {
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Close Icon

Pass a `closeIcon` field to customize the close button [Icon](https://ui.nuxt.com/docs/components/icon). Default to `i-lucide-x`.

```vue [ToastCloseIconExample.vue]
<script setup lang="ts">
const props = defineProps<{
  closeIcon: string
}>()

const toast = useToast()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: 'There was a problem with your request.',
    closeIcon: props.closeIcon
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.close\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.close\` key.

### Actions

Pass an `actions` field to add some [Button](https://ui.nuxt.com/docs/components/button) actions to the Toast.

```vue [ToastActionsExample.vue]
<script setup lang="ts">
const toast = useToast()

const props = defineProps<{
  description: string
}>()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: props.description,
    actions: [{
      icon: 'i-lucide-refresh-cw',
      label: 'Retry',
      color: 'neutral',
      variant: 'outline',
      onClick: (e) => {
        e?.stopPropagation()
      }
    }]
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Progress

Pass a `progress` field to customize or hide the [Progress](https://ui.nuxt.com/docs/components/progress) bar (with `false` value).

\> \[!TIP]
\> The Progress bar inherits the Toast color by default, but you can override it using the \`progress.color\` field.

```vue [ToastProgressExample.vue]
<script setup lang="ts">
const toast = useToast()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    description: 'There was a problem with your request.',
    icon: 'i-lucide-wifi',
    progress: false
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### Orientation

Pass an `orientation` field to the `toast.add` method to change the orientation of the Toast.

```vue [ToastOrientationExample.vue]
<script setup lang="ts">
const toast = useToast()

const props = defineProps<{
  orientation: 'horizontal' | 'vertical'
}>()

function showToast() {
  toast.add({
    title: 'Uh oh! Something went wrong.',
    orientation: props.orientation,
    actions: [{
      icon: 'i-lucide-refresh-cw',
      label: 'Retry',
      color: 'neutral',
      variant: 'outline',
      onClick: (e) => {
        e?.stopPropagation()
      }
    }]
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

## Examples

\> \[!NOTE]
\> See: /components/app
\> Nuxt UI provides an App component that wraps your app to provide global configurations.

### Change global position

Change the `toaster.position` prop on the [App](https://ui.nuxt.com/docs/components/app#props) component to change the position of the toasts.

```vue [app.vue]
<script setup lang="ts">
const toaster = { position: 'bottom-right' }
</script>

<template>
  <UApp :toaster="toaster">
    <NuxtPage />
  </UApp>
</template>
```

```vue [ToastExample.vue]
<script setup lang="ts">
const toast = useToast()

function addToCalendar() {
  const eventDate = new Date(Date.now() + Math.random() * 31536000000)
  const formattedDate = eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })

  toast.add({
    title: 'Event added to calendar',
    description: `This event is scheduled for ${formattedDate}.`,
    icon: 'i-lucide-calendar-days'
  })
}
</script>

<template>
  <UButton label="Add to calendar" color="neutral" variant="outline" icon="i-lucide-plus" @click="addToCalendar" />
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/docs/app/app.config.ts#L3
\> In this example, we use the \`AppConfig\` to configure the \`position\` prop of the \`Toaster\` component globally.

### Change global duration

Change the `toaster.duration` prop on the [App](https://ui.nuxt.com/docs/components/app#props) component to change the duration of the toasts.

```vue [app.vue]
<script setup lang="ts">
const toaster = { duration: 5000 }
</script>

<template>
  <UApp :toaster="toaster">
    <NuxtPage />
  </UApp>
</template>
```

```vue [ToastExample.vue]
<script setup lang="ts">
const toast = useToast()

function addToCalendar() {
  const eventDate = new Date(Date.now() + Math.random() * 31536000000)
  const formattedDate = eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })

  toast.add({
    title: 'Event added to calendar',
    description: `This event is scheduled for ${formattedDate}.`,
    icon: 'i-lucide-calendar-days'
  })
}
</script>

<template>
  <UButton label="Add to calendar" color="neutral" variant="outline" icon="i-lucide-plus" @click="addToCalendar" />
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/docs/app/app.config.ts#L4
\> In this example, we use the \`AppConfig\` to configure the \`duration\` prop of the \`Toaster\` component globally.

### Change global max `4.1+`

Change the `toaster.max` prop on the [App](https://ui.nuxt.com/docs/components/app#props) component to change the max number of toasts displayed at once.

```vue [app.vue]
<script setup lang="ts">
const toaster = { max: 3 }
</script>

<template>
  <UApp :toaster="toaster">
    <NuxtPage />
  </UApp>
</template>
```

```vue [ToastExample.vue]
<script setup lang="ts">
const toast = useToast()

function addToCalendar() {
  const eventDate = new Date(Date.now() + Math.random() * 31536000000)
  const formattedDate = eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })

  toast.add({
    title: 'Event added to calendar',
    description: `This event is scheduled for ${formattedDate}.`,
    icon: 'i-lucide-calendar-days'
  })
}
</script>

<template>
  <UButton label="Add to calendar" color="neutral" variant="outline" icon="i-lucide-plus" @click="addToCalendar" />
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/docs/app/app.config.ts#L5
\> In this example, we use the \`AppConfig\` to configure the \`max\` prop of the \`Toaster\` component globally.

### Stacked toasts

Set the `toaster.expand` prop to `false` on the [App](https://ui.nuxt.com/docs/components/app#props) component to display stacked toasts (inspired by [Sonner](https://sonner.emilkowal.ski/){rel="&#x22;nofollow&#x22;"}).

```vue [app.vue]
<script setup lang="ts">
const toaster = { expand: true }
</script>

<template>
  <UApp :toaster="toaster">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!TIP]
\> You can hover over the toasts to expand them. This will also pause the timer of the toasts.

```vue [ToastExample.vue]
<script setup lang="ts">
const toast = useToast()

function addToCalendar() {
  const eventDate = new Date(Date.now() + Math.random() * 31536000000)
  const formattedDate = eventDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })

  toast.add({
    title: 'Event added to calendar',
    description: `This event is scheduled for ${formattedDate}.`,
    icon: 'i-lucide-calendar-days'
  })
}
</script>

<template>
  <UButton label="Add to calendar" color="neutral" variant="outline" icon="i-lucide-plus" @click="addToCalendar" />
</template>
```

\> \[!NOTE]
\> See: https\://github.com/nuxt/ui/blob/v4/docs/app/app.config.ts#L6
\> In this example, we use the \`AppConfig\` to configure the \`expand\` prop of the \`Toaster\` component globally.

### Deduplicated toasts `4.5+`

When calling `toast.add` with an `id` that already exists, the existing toast will pulse instead of creating a duplicate.

```vue [ToastDuplicateExample.vue]
<script setup lang="ts">
const toast = useToast()

function showToast() {
  toast.add({
    id: 'copy',
    title: 'Copied to clipboard',
    icon: 'i-lucide-circle-check',
    duration: 0
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### With callback

Pass an `onUpdateOpen` field to execute a callback when the toast is closed (either by expiration or user dismissal).

```vue [ToastCallbackExample.vue]
<script setup lang="ts">
const toast = useToast()

function showToast() {
  const t = toast.add({
    'title': 'Uploading file...',
    'description': 'Your file is being uploaded.',
    'icon': 'i-lucide-cloud-upload',
    'duration': 3000,
    'onUpdate:open': (open: boolean) => {
      if (open) {
        return
      }

      toast.update(t.id, {
        'title': 'File uploaded!',
        'description': 'Your file has been successfully uploaded.',
        'icon': 'i-lucide-check',
        'color': 'success',
        'onUpdate:open': undefined
      })
    }
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

### With HTML content

Use the [`h()` render function](https://vuejs.org/api/render-function.html#h){rel="&#x22;nofollow&#x22;"} in the `title` or `description` fields to render HTML elements or Vue components with custom styling.

```vue [ToastHtmlExample.vue]
<script setup lang="ts">
const toast = useToast()

function showToast() {
  toast.add({
    title: h('span', {}, [
      'Item ',
      h('span', { class: 'text-primary font-bold' }, '#15'),
      ' deleted'
    ]),
    description: h('span', {}, [
      'You have successfully deleted the item from your ',
      h('span', { class: 'font-bold' }, 'account'),
      '.'
    ]),
    icon: 'i-lucide-trash-2'
  })
}
</script>

<template>
  <UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
</template>
```

## API

### Props

```ts
/**
 * Props for the Toast component
 */
interface ToastProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  title?: StringOrVNode | undefined;
  description?: StringOrVNode | undefined;
  icon?: any;
  avatar?: AvatarProps | undefined;
  color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" | undefined;
  /**
   * The orientation between the content and the actions.
   * @default "\"vertical\""
   */
  orientation?: "vertical" | "horizontal" | undefined;
  /**
   * Display a close button to dismiss the toast.
   * `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
   * @default "true"
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   */
  closeIcon?: any;
  /**
   * Display a list of actions:
   * - under the title and description when orientation is `vertical`
   * - next to the close button when orientation is `horizontal`
   * `{ size: 'xs' }`{lang="ts-type"}
   */
  actions?: ButtonProps[] | undefined;
  /**
   * Display a progress bar showing the toast's remaining duration.
   * `{ size: 'sm' }`{lang="ts-type"}
   * @default "true"
   */
  progress?: boolean | Pick<ProgressProps, "color" | "ui"> | undefined;
  ui?: { root?: ClassNameValue; wrapper?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; icon?: ClassNameValue; avatar?: ClassNameValue; avatarSize?: ClassNameValue; actions?: ClassNameValue; progress?: ClassNameValue; close?: ClassNameValue; } | undefined;
  /**
   * The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the dialog. Can be bind as `v-model:open`.
   */
  open?: boolean | undefined;
  /**
   * Control the sensitivity of the toast for accessibility purposes.
   * 
   * For toasts that are the result of a user action, choose `foreground`. Toasts generated from background tasks should use `background`.
   */
  type?: "foreground" | "background" | undefined;
  /**
   * Time in milliseconds that toast should remain visible for. Overrides value
   * given to `ToastProvider`.
   */
  duration?: number | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Toast component
 */
interface ToastSlots {
  leading(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Toast component
 */
interface ToastEmits {
  pause: (payload: []) => void;
  escapeKeyDown: (payload: [event: KeyboardEvent]) => void;
  resume: (payload: []) => void;
  swipeStart: (payload: [event: SwipeEvent]) => void;
  swipeMove: (payload: [event: SwipeEvent]) => void;
  swipeCancel: (payload: [event: SwipeEvent]) => void;
  swipeEnd: (payload: [event: SwipeEvent]) => void;
  update:open: (payload: [value: boolean]) => void;
}
```

### Expose

When accessing the component via a template ref, you can use the following:

| Name                                                                                                                         | Type                                                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `height`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} | `Ref<number>`{.language-ts-type.shiki.shiki-themes.material-theme-lighter.material-theme.material-theme-palenight lang="ts-type"} |

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    toast: {
      slots: {
        root: 'relative group overflow-hidden bg-default shadow-lg rounded-lg ring ring-default p-4 flex gap-2.5 focus:outline-none',
        wrapper: 'w-0 flex-1 flex flex-col',
        title: 'text-sm font-medium text-highlighted',
        description: 'text-sm text-muted',
        icon: 'shrink-0 size-5',
        avatar: 'shrink-0',
        avatarSize: '2xl',
        actions: 'flex gap-1.5 shrink-0',
        progress: 'absolute inset-x-0 bottom-0',
        close: 'p-0'
      },
      variants: {
        color: {
          primary: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
            icon: 'text-primary'
          },
          secondary: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-secondary',
            icon: 'text-secondary'
          },
          success: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-success',
            icon: 'text-success'
          },
          info: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-info',
            icon: 'text-info'
          },
          warning: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-warning',
            icon: 'text-warning'
          },
          error: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-error',
            icon: 'text-error'
          },
          neutral: {
            root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted',
            icon: 'text-highlighted'
          }
        },
        orientation: {
          horizontal: {
            root: 'items-center',
            actions: 'items-center'
          },
          vertical: {
            root: 'items-start',
            actions: 'items-start mt-2.5'
          }
        },
        title: {
          true: {
            description: 'mt-1'
          }
        }
      },
      defaultVariants: {
        color: 'primary'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Toast.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/toast.ts).


# Tooltip

## Usage

Use a [Button](https://ui.nuxt.com/docs/components/button) or any other component in the default slot of the Tooltip.

```vue
<template>
  <UTooltip text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

\> \[!WARNING]
\> Make sure to wrap your app with the \[\`App\`]\(/docs/components/app) component which uses the \[\`TooltipProvider\`]\(https\://reka-ui.com/docs/components/tooltip#provider) component from Reka UI.

\> \[!TIP]
\> See: /docs/components/app#props
\> You can check the \`App\` component \`tooltip\` prop to see how to configure the Tooltip globally.

### Text

Use the `text` prop to set the content of the Tooltip.

```vue
<template>
  <UTooltip text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

### Kbds

Use the `kbds` prop to render [Kbd](https://ui.nuxt.com/docs/components/kbd) components in the Tooltip.

```vue
<template>
  <UTooltip text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

\> \[!TIP]
\> You can use special keys like \`meta\` that displays as \`⌘\` on macOS and \`Ctrl\` on other platforms.

### Delay

Use the `delay-duration` prop to change the delay before the Tooltip appears. For example, you can make it appear instantly by setting it to `0`.

```vue
<template>
  <UTooltip :delay-duration="0" text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

\> \[!TIP]
\> This can be configured globally through the \`tooltip.delayDuration\` option in the \[\`App\`]\(/docs/components/app) component.

### Content

Use the `content` prop to control how the Tooltip content is rendered, like its `align` or `side` for example.

\> \[!TIP]
\> This can be configured globally through the \`tooltip.content\` option in the \[\`App\`]\(/docs/components/app) component.

```vue
<template>
  <UTooltip text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

### Arrow

Use the `arrow` prop to display an arrow on the Tooltip.

```vue
<template>
  <UTooltip arrow text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

### Disabled

Use the `disabled` prop to disable the Tooltip.

```vue
<template>
  <UTooltip disabled text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

## Examples

### Control open state

You can control the open state by using the `default-open` prop or the `v-model:open` directive.

```vue [TooltipOpenExample.vue]
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UTooltip v-model:open="open" text="Open on GitHub">
    <UButton label="Open" color="neutral" variant="subtle" />
  </UTooltip>
</template>
```

\> \[!NOTE]
\> In this example, leveraging \[\`defineShortcuts\`]\(/docs/composables/define-shortcuts), you can toggle the Tooltip by pressing .

### With following cursor

You can make the Tooltip follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger){rel="&#x22;nofollow&#x22;"} prop:

```vue [TooltipCursorExample.vue]
<script setup lang="ts">
const open = ref(false)
const anchor = ref({ x: 0, y: 0 })

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value
    } as DOMRect)
}))
</script>

<template>
  <UTooltip
    :open="open"
    :reference="reference"
    :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
      @pointerenter="open = true"
      @pointerleave="open = false"
      @pointermove="(ev: PointerEvent) => {
        anchor.x = ev.clientX
        anchor.y = ev.clientY
      }"
    >
      Hover me
    </div>

    <template #content>
      {{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
    </template>
  </UTooltip>
</template>
```

## API

### Props

```ts
/**
 * Props for the Tooltip component
 */
interface TooltipProps {
  /**
   * The text content of the tooltip.
   */
  text?: string | undefined;
  /**
   * The keyboard keys to display in the tooltip.
   */
  kbds?: (string | undefined)[] | KbdProps[] | undefined;
  /**
   * The content of the tooltip.
   * 
   * Inherits from the `tooltip.content` of the {@link AppProps } component when not provided.
   */
  content?: (Omit<TooltipContentProps, "as" | "asChild"> & Partial<EmitsToProps<TooltipContentImplEmits>>) | undefined;
  /**
   * Display an arrow alongside the tooltip.
   * `{ rounded: true }`{lang="ts-type"}
   */
  arrow?: boolean | Omit<TooltipArrowProps, "as" | "asChild"> | undefined;
  /**
   * Render the tooltip in a portal.
   * @default "true"
   */
  portal?: string | boolean | HTMLElement | undefined;
  /**
   * The reference (or anchor) element that is being referred to for positioning.
   * 
   * If not provided will use the current component as anchor.
   */
  reference?: ReferenceElement | undefined;
  ui?: { content?: ClassNameValue; arrow?: ClassNameValue; text?: ClassNameValue; kbds?: ClassNameValue; kbdsSize?: ClassNameValue; } | undefined;
  /**
   * The open state of the tooltip when it is initially rendered.
   * Use when you do not need to control its open state.
   */
  defaultOpen?: boolean | undefined;
  /**
   * The controlled open state of the tooltip.
   */
  open?: boolean | undefined;
  /**
   * Override the duration given to the `Provider` to customise
   * the open delay for a specific tooltip.
   */
  delayDuration?: number | undefined;
  /**
   * Prevents Tooltip.Content from remaining open when hovering.
   * Disabling this has accessibility consequences. Inherits
   * from Tooltip.Provider.
   */
  disableHoverableContent?: boolean | undefined;
  /**
   * When `true`, clicking on trigger will not close the content.
   */
  disableClosingTrigger?: boolean | undefined;
  /**
   * When `true`, disable tooltip
   */
  disabled?: boolean | undefined;
  /**
   * Prevent the tooltip from opening if the focus did not come from
   * the keyboard by matching against the `:focus-visible` selector.
   * This is useful if you want to avoid opening it when switching
   * browser tabs or closing a dialog.
   */
  ignoreNonKeyboardFocus?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Tooltip component
 */
interface TooltipSlots {
  default(): any;
  content(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Tooltip component
 */
interface TooltipEmits {
  update:open: (payload: [value: boolean]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    tooltip: {
      slots: {
        content: 'flex items-center gap-1 bg-default text-highlighted shadow-sm rounded-sm ring ring-default h-6 px-2.5 py-1 text-xs select-none data-[state=delayed-open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-tooltip-content-transform-origin) pointer-events-auto',
        arrow: 'fill-bg stroke-default',
        text: 'truncate',
        kbds: "hidden lg:inline-flex items-center shrink-0 gap-0.5 not-first-of-type:before:content-['·'] not-first-of-type:before:me-0.5",
        kbdsSize: 'sm'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Tooltip.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/tooltip.ts).


# Tree

## Usage

Use the Tree component to display a hierarchical structure of items.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree :items="items" />
</template>
```

### Items

Use the `items` prop as an array of objects with the following properties:

- `icon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `label?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `trailingIcon?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `defaultExpanded?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `disabled?: boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `slot?: string`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `children?: TreeItem[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `onToggle?: (e: TreeItemToggleEvent<TreeItem>) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `onSelect?: (e: TreeItemSelectEvent<TreeItem>) => void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `class?: any`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemWithChildren?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingIcon?: ClassNameValue, listWithChildren?: ClassNameValue }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

\> \[!NOTE]
\> A unique identifier is required for each item. The component will use the \`label\` prop as identifier if no \`get-key\` is provided. Ideally you should provide a \`get-key\` function prop to return a unique identifier. Alternatively, you can use the \`labelKey\` prop to specify which property to use as the unique identifier.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree :items="items" />
</template>
```

### Multiple

Use the `multiple` prop to allow multiple item selections.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree multiple :items="items" />
</template>
```

### Nested `4.1+`

Use the `nested` prop to control whether the Tree is rendered with nested structure or as a flat list. Defaults to `true`.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree :nested="false" :items="items" />
</template>
```

\> \[!NOTE]
\> See: #with-virtualization
\> When \`nested\` is \`false\`, all items are rendered at the same level with indentation to indicate hierarchy. This is useful for virtualization or drag and drop functionality.

### Color

Use the `color` prop to change the color of the Tree.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree color="neutral" :items="items" />
</template>
```

### Size

Use the `size` prop to change the size of the Tree.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree size="xl" :items="items" />
</template>
```

### Trailing Icon

Use the `trailing-icon` prop to customize the trailing [Icon](https://ui.nuxt.com/docs/components/icon) of a parent node. Defaults to `i-lucide-chevron-down`.

\> \[!NOTE]
\> If an icon is specified for an item, it will always take precedence over these props.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        trailingIcon: 'i-lucide-chevron-down',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree trailing-icon="i-lucide-arrow-down" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize this icon globally in your \`app.config.ts\` under \`ui.icons.chevronDown\` key.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize this icon globally in your \`vite.config.ts\` under \`ui.icons.chevronDown\` key.

### Expanded Icon

Use the `expanded-icon` and `collapsed-icon` props to customize the icons of a parent node when it is expanded or collapsed. Defaults to `i-lucide-folder-open` and `i-lucide-folder` respectively.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          {
            label: 'Card.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
          {
            label: 'Button.vue',
            icon: 'i-vscode-icons-file-type-vue',
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree expanded-icon="i-lucide-book-open" collapsed-icon="i-lucide-book" :items="items" />
</template>
```

\*\*Nuxt:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/nuxt#theme
\> You can customize these icons globally in your \`app.config.ts\` under \`ui.icons.folder\` and \`ui.icons.folderOpen\` keys.
\*\*Vue:\*\*
\> \[!TIP]
\> See: /docs/getting-started/integrations/icons/vue#theme
\> You can customize these icons globally in your \`vite.config.ts\` under \`ui.icons.folder\` and \`ui.icons.folderOpen\` keys.

### Disabled

Use the `disabled` prop to prevent any user interaction with the Tree.

```vue
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = ref<TreeItem[]>([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultExpanded: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
          {
            label: 'useUser.ts',
            icon: 'i-vscode-icons-file-type-typescript',
          },
        ],
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'i-vscode-icons-file-type-vue',
              },
              {
                label: 'Button.vue',
                icon: 'i-vscode-icons-file-type-vue',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    label: 'app.vue',
    icon: 'i-vscode-icons-file-type-vue',
  },
  {
    label: 'nuxt.config.ts',
    icon: 'i-vscode-icons-file-type-nuxt',
  },
])
</script>

<template>
  <UTree disabled :items="items" />
</template>
```

\> \[!NOTE]
\> You can also disable individual items using \`item.disabled\`.

## Examples

### Control selected item(s)

You can control the selected item(s) by using the `default-value` prop or the `v-model` directive.

```vue [TreeModelValueExample.vue]
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items: TreeItem[] = [
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
]

const value = ref()
</script>

<template>
  <UTree v-model="value" :items="items" />
</template>
```

\> \[!TIP]
\> Use the \`get-key\` prop to change the function used to get the unique key from each item when a \`v-model\` or \`default-value\` is provided.

If you want to prevent an item from being selected, you can use the `item.onSelect()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} property or the global `select` event:

```vue [TreeOnSelectExample.vue]
<script setup lang="ts">
import type { TreeItemSelectEvent } from 'reka-ui'
import type { TreeItem } from '@nuxt/ui'

const items: TreeItem[] = [
  {
    label: 'app/',
    defaultExpanded: true,
    onSelect: (e: Event) => {
      e.preventDefault()
    },
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
]

function onSelect(e: TreeItemSelectEvent<TreeItem>) {
  if (e.detail.originalEvent.type === 'click') {
    e.preventDefault()
  }
}
</script>

<template>
  <UTree :items="items" @select="onSelect" />
</template>
```

\> \[!NOTE]
\> This lets you expand or collapse a parent item without selecting it.

### Control expanded items

You can control the expanded items by using the `default-expanded` prop or the `v-model` directive.

```vue [TreeExpandedExample.vue]
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = [
  {
    label: 'app/',
    id: 'app',
    children: [
      {
        label: 'composables/',
        id: 'app/composables',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        id: 'app/components',
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', id: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', id: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
] satisfies TreeItem[]

const expanded = ref(['app', 'app/composables'])
</script>

<template>
  <UTree v-model:expanded="expanded" :items="items" :get-key="i => i.id" />
</template>
```

If you want to prevent an item from being expanded, you can use the `item.onToggle()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"} property or the global `toggle` event:

```vue [TreeOnToggleExample.vue]
<script setup lang="ts">
import type { TreeItemToggleEvent } from 'reka-ui'
import type { TreeItem } from '@nuxt/ui'

const items: TreeItem[] = [
  {
    label: 'app/',
    defaultExpanded: true,
    onToggle: (e: Event) => {
      e.preventDefault()
    },
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
]

function onToggle(e: TreeItemToggleEvent<TreeItem>) {
  if (e.detail.originalEvent.type === 'keydown') {
    e.preventDefault()
  }
}
</script>

<template>
  <UTree :items="items" @toggle="onToggle" />
</template>
```

\> \[!NOTE]
\> This lets you select a parent item without expanding or collapsing its children.

### With checkbox in items `4.1+`

You can use the `item-leading` slot to add a [Checkbox](https://ui.nuxt.com/docs/components/checkbox) to the items. Use the `multiple`, `propagate-select` and `bubble-select` props to enable multi-selection with parent-child relationship and the `select` and `toggle` events to control the selected and expanded state of the items.

```vue [TreeCheckboxItemsExample.vue]
<script setup lang="ts">
import type { TreeItemSelectEvent } from 'reka-ui'
import type { TreeItem } from '@nuxt/ui'

const items: TreeItem[] = [
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts' },
          { label: 'useUser.ts' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue' },
          { label: 'Button.vue' }
        ]
      }
    ]
  },
  { label: 'app.vue' },
  { label: 'nuxt.config.ts' }
]

const value = ref<(typeof items)>([])

function onSelect(e: TreeItemSelectEvent<TreeItem>) {
  if (e.detail.originalEvent.type === 'click') {
    e.preventDefault()
  }
}
</script>

<template>
  <UTree
    v-model="value"
    :as="{ link: 'div' }"
    :items="items"
    multiple
    propagate-select
    bubble-select
    @select="onSelect"
  >
    <template #item-leading="{ selected, indeterminate, handleSelect }">
      <UCheckbox
        :model-value="indeterminate ? 'indeterminate' : selected"
        tabindex="-1"
        @change="handleSelect"
        @click.stop
      />
    </template>
  </UTree>
</template>
```

\> \[!NOTE]
\> This example uses the \`as\` prop to change the items from \`button\` to \`div\` as the \[\`Checkbox\`]\(/docs/components/checkbox) is also rendered as a \`button\`.

### With drag and drop `4.1+`

Use the [`useSortable`](https://vueuse.org/integrations/useSortable/){rel="&#x22;nofollow&#x22;"} composable from [`@vueuse/integrations`](https://vueuse.org/integrations/README.html){rel="&#x22;nofollow&#x22;"} to enable drag and drop functionality on the Tree. This integration wraps [Sortable.js](https://sortablejs.github.io/Sortable/){rel="&#x22;nofollow&#x22;"} to provide a seamless drag and drop experience.

```vue [TreeDragAndDropExample.vue]
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'

const items = shallowRef<TreeItem[]>([
  {
    label: 'app/',
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
])

function flatten(items: TreeItem[], parent = items): { item: TreeItem, parent: TreeItem[], index: number }[] {
  return items.flatMap((item, index) => [
    { item, parent, index },
    ...(item.children?.length && item.defaultExpanded ? flatten(item.children, item.children) : [])
  ])
}

function moveItem(oldIndex: number, newIndex: number) {
  if (oldIndex === newIndex) return

  const flat = flatten(items.value)
  const source = flat[oldIndex]
  const target = flat[newIndex]

  if (!source || !target) return

  const [moved] = source.parent.splice(source.index, 1)
  if (!moved) return

  const updatedFlat = flatten(items.value)
  const updatedTarget = updatedFlat.find(({ item }) => item === target.item)
  if (!updatedTarget) return

  const insertIndex = oldIndex < newIndex ? updatedTarget.index + 1 : updatedTarget.index
  updatedTarget.parent.splice(insertIndex, 0, moved)
}

const tree = useTemplateRef<HTMLElement>('tree')

useSortable(tree, items, {
  animation: 150,
  ghostClass: 'opacity-50',
  onUpdate: (e: any) => moveItem(e.oldIndex, e.newIndex)
})
</script>

<template>
  <UTree ref="tree" :nested="false" :unmount-on-hide="false" :items="items" />
</template>
```

\> \[!NOTE]
\> This example sets the \`nested\` prop to \`false\` to have a flat list of items so that the items can be dragged and dropped.

### With virtualization `4.1+`

Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.

\> \[!WARNING]
\> When virtualization is enabled, the tree structure is flattened, similar to setting the \`nested\` prop to \`false\`.

```vue [TreeVirtualizeExample.vue]
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items: TreeItem[] = Array(1000).fill(0).map((_, i) => ({
  label: `Item ${i + 1}`,
  children: [
    { label: `Child ${i + 1}-1`, icon: 'i-lucide-file' },
    { label: `Child ${i + 1}-2`, icon: 'i-lucide-file' }
  ]
}))
</script>

<template>
  <UTree virtualize :items="items" class="h-80" />
</template>
```

### With custom slot

Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}-wrapper`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-leading`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-label`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}
- `#{{ item.slot }}-trailing`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

```vue [TreeCustomSlotExample.vue]
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'

const items = [
  {
    label: 'app/',
    slot: 'app' as const,
    defaultExpanded: true,
    children: [
      {
        label: 'composables/',
        children: [
          { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
          { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
        ]
      },
      {
        label: 'components/',
        defaultExpanded: true,
        children: [
          { label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
          { label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
        ]
      }
    ]
  },
  { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
] satisfies TreeItem[]
</script>

<template>
  <UTree :items="items">
    <template #app="{ item }">
      <p class="italic font-bold">
        {{ item.label }}
      </p>
    </template>
  </UTree>
</template>
```

## API

### Props

```ts
/**
 * Props for the Tree component
 */
interface TreeProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
  size?: "md" | "xs" | "sm" | "lg" | "xl" | undefined;
  /**
   * This function is passed the index of each item and should return a unique key for that item
   */
  getKey?: ((val: T[number]) => string) | undefined;
  /**
   * The key used to get the label from the item.
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * The icon displayed on the right side of a parent node.
   */
  trailingIcon?: any;
  /**
   * The icon displayed when a parent node is expanded.
   */
  expandedIcon?: any;
  /**
   * The icon displayed when a parent node is collapsed.
   */
  collapsedIcon?: any;
  items?: T | undefined;
  /**
   * The controlled value of the Tree. Can be bind as `v-model`.
   */
  modelValue?: (M extends true ? T[number][] : T[number]) | undefined;
  /**
   * The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree.
   */
  defaultValue?: (M extends true ? T[number][] : T[number]) | undefined;
  /**
   * Whether multiple options can be selected or not.
   */
  multiple?: M | undefined;
  /**
   * Use nested DOM structure (children inside parents) vs flattened structure (all items at same level).
   * When `virtualize` is enabled, this is automatically set to `false`.
   * @default "true"
   */
  nested?: boolean | undefined;
  /**
   * Enable virtualization for large lists.
   * Note: when enabled, the tree structure is flattened like if `nested` was set to `false`.
   * @default "false"
   */
  virtualize?: boolean | { overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; } | undefined;
  onSelect?: ((e: SelectEvent<T[number]>, item: T[number]) => void) | undefined;
  onToggle?: ((e: ToggleEvent<T[number]>, item: T[number]) => void) | undefined;
  ui?: { root?: ClassNameValue; item?: ClassNameValue; listWithChildren?: ClassNameValue; itemWithChildren?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkTrailing?: ClassNameValue; linkTrailingIcon?: ClassNameValue; } | undefined;
  /**
   * The controlled value of the expanded item. Can be binded with `v-model`.
   */
  expanded?: string[] | undefined;
  /**
   * The value of the expanded tree when initially rendered. Use when you do not need to control the state of the expanded tree
   */
  defaultExpanded?: string[] | undefined;
  /**
   * How multiple selection should behave in the collection.
   */
  selectionBehavior?: "replace" | "toggle" | undefined;
  /**
   * When `true`, selecting parent will select the descendants.
   */
  propagateSelect?: boolean | undefined;
  /**
   * When `true`, prevents the user from interacting with tree
   */
  disabled?: boolean | undefined;
  /**
   * When `true`, selecting children will update the parent state.
   */
  bubbleSelect?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Tree component
 */
interface TreeSlots {
  item-wrapper(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-trailing(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Tree component
 */
interface TreeEmits {
  update:modelValue: (payload: [val: M extends true ? T[number][] : T[number]]) => void;
  update:expanded: (payload: [val: string[]]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    tree: {
      slots: {
        root: 'relative isolate',
        item: 'w-full',
        listWithChildren: 'border-s border-default',
        itemWithChildren: 'ps-1.5 -ms-px',
        link: 'relative group w-full flex items-center text-sm select-none before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
        linkLeadingIcon: 'shrink-0 relative',
        linkLabel: 'truncate',
        linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        linkTrailingIcon: 'shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180'
      },
      variants: {
        virtualize: {
          true: {
            root: 'overflow-y-auto'
          }
        },
        color: {
          primary: {
            link: 'focus-visible:before:ring-primary'
          },
          secondary: {
            link: 'focus-visible:before:ring-secondary'
          },
          success: {
            link: 'focus-visible:before:ring-success'
          },
          info: {
            link: 'focus-visible:before:ring-info'
          },
          warning: {
            link: 'focus-visible:before:ring-warning'
          },
          error: {
            link: 'focus-visible:before:ring-error'
          },
          neutral: {
            link: 'focus-visible:before:ring-inverted'
          }
        },
        size: {
          xs: {
            listWithChildren: 'ms-4',
            link: 'px-2 py-1 text-xs gap-1',
            linkLeadingIcon: 'size-4',
            linkTrailingIcon: 'size-4'
          },
          sm: {
            listWithChildren: 'ms-4.5',
            link: 'px-2.5 py-1.5 text-xs gap-1.5',
            linkLeadingIcon: 'size-4',
            linkTrailingIcon: 'size-4'
          },
          md: {
            listWithChildren: 'ms-5',
            link: 'px-2.5 py-1.5 text-sm gap-1.5',
            linkLeadingIcon: 'size-5',
            linkTrailingIcon: 'size-5'
          },
          lg: {
            listWithChildren: 'ms-5.5',
            link: 'px-3 py-2 text-sm gap-2',
            linkLeadingIcon: 'size-5',
            linkTrailingIcon: 'size-5'
          },
          xl: {
            listWithChildren: 'ms-6',
            link: 'px-3 py-2 text-base gap-2',
            linkLeadingIcon: 'size-6',
            linkTrailingIcon: 'size-6'
          }
        },
        selected: {
          true: {
            link: 'before:bg-elevated'
          }
        },
        disabled: {
          true: {
            link: 'cursor-not-allowed opacity-75'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          selected: true,
          class: {
            link: 'text-primary'
          }
        },
        {
          color: 'secondary',
          selected: true,
          class: {
            link: 'text-secondary'
          }
        },
        {
          color: 'success',
          selected: true,
          class: {
            link: 'text-success'
          }
        },
        {
          color: 'info',
          selected: true,
          class: {
            link: 'text-info'
          }
        },
        {
          color: 'warning',
          selected: true,
          class: {
            link: 'text-warning'
          }
        },
        {
          color: 'error',
          selected: true,
          class: {
            link: 'text-error'
          }
        },
        {
          color: 'neutral',
          selected: true,
          class: {
            link: 'text-highlighted'
          }
        },
        {
          selected: false,
          disabled: false,
          class: {
            link: [
              'hover:text-highlighted hover:before:bg-elevated/50',
              'transition-colors before:transition-colors'
            ]
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/Tree.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/tree.ts).


# User

## Usage

### Name

Use the `name` prop to display a name for the user.

```vue
<template>
  <UUser name="John Doe" />
</template>
```

### Description

Use the `description` prop to display a description for the user.

```vue
<template>
  <UUser name="John Doe" description="Software Engineer" />
</template>
```

### Avatar

Use the `avatar` prop to display an [Avatar](https://ui.nuxt.com/docs/components/avatar) component.

```vue
<template>
  <UUser name="John Doe" description="Software Engineer" />
</template>
```

\`\`\`ts
/\*\*
\* Props for the Avatar component
\*/
interface AvatarProps {
/\*\*
\* The element or component this component should render as.
\*/
as?: any;
src?: string | undefined;
alt?: string | undefined;
icon?: any;
text?: string | undefined;
size?: "2xl" | "md" | "3xs" | "2xs" | "xs" | "sm" | "lg" | "xl" | "3xl" | undefined;
chip?: boolean | ChipProps | undefined;
ui?: { root?: ClassNameValue; image?: ClassNameValue; fallback?: ClassNameValue; icon?: ClassNameValue; } | undefined;
loading?: "lazy" | "eager" | undefined;
referrerpolicy?: HTMLAttributeReferrerPolicy | undefined;
crossorigin?: "" | "anonymous" | "use-credentials" | undefined;
decoding?: "async" | "auto" | "sync" | undefined;
height?: Numberish | undefined;
sizes?: string | undefined;
srcset?: string | undefined;
usemap?: string | undefined;
width?: Numberish | undefined;
}
\`\`\`

### Chip

Use the `chip` prop to display a [Chip](https://ui.nuxt.com/docs/components/chip) component.

```vue
<template>
  <UUser name="John Doe" description="Software Engineer" />
</template>
```

\`\`\`ts
/\*\*
\* Props for the Chip component
\*/
interface ChipProps {
/\*\*
\* The element or component this component should render as.
\*/
as?: any;
/\*\*
\* Display some text inside the chip.
\*/
text?: string | number | undefined;
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" | undefined;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "3xs" | "2xs" | "2xl" | "3xl" | undefined;
/\*\*
\* The position of the chip.
\*/
position?: "top-right" | "bottom-right" | "top-left" | "bottom-left" | undefined;
/\*\*
\* When \`true\`, keep the chip inside the component for rounded elements.
\* @default "false"
\*/
inset?: boolean | undefined;
/\*\*
\* When \`true\`, render the chip relatively to the parent.
\* @default "false"
\*/
standalone?: boolean | undefined;
ui?: { root?: ClassNameValue; base?: ClassNameValue; } | undefined;
/\*\*
\* @default "true"
\*/
show?: boolean | undefined;
}
\`\`\`

### Size

Use the `size` prop to change the size of the user avatar and text.

```vue
<template>
  <UUser name="John Doe" description="Software Engineer" chip size="xl" />
</template>
```

### Orientation

Use the `orientation` prop to change the orientation. Defaults to `horizontal`.

```vue
<template>
  <UUser orientation="vertical" name="John Doe" description="Software Engineer" />
</template>
```

### Link

You can pass any property from the [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link){rel="&#x22;nofollow&#x22;"} component such as `to`, `target`, `rel`, etc.

```vue
<template>
  <UUser to="https://github.com/benjamincanac" target="_blank" name="Benjamin Canac" description="Software Engineer" />
</template>
```

\> \[!NOTE]
\> The \`NuxtLink\` component will inherit all other attributes you pass to the \`User\` component.

## API

### Props

```ts
/**
 * Props for the User component
 */
interface UserProps {
  /**
   * The element or component this component should render as.
   */
  as?: any;
  name?: string | undefined;
  description?: string | undefined;
  avatar?: (Omit<AvatarProps, "size"> & { [key: string]: any; }) | undefined;
  chip?: boolean | Omit<ChipProps, "size" | "inset"> | undefined;
  size?: "md" | "3xs" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl" | undefined;
  /**
   * The orientation of the user.
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  to?: string | St | vt | undefined;
  target?: "_blank" | "_parent" | "_self" | "_top" | (string & {}) | null | undefined;
  onClick?: ((event: MouseEvent) => void | Promise<void>) | undefined;
  ui?: { root?: ClassNameValue; wrapper?: ClassNameValue; name?: ClassNameValue; description?: ClassNameValue; avatar?: ClassNameValue; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the User component
 */
interface UserSlots {
  avatar(): any;
  name(): any;
  description(): any;
  default(): any;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    user: {
      slots: {
        root: 'relative group/user',
        wrapper: '',
        name: 'font-medium',
        description: 'text-muted',
        avatar: 'shrink-0'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'flex items-center'
          },
          vertical: {
            root: 'flex flex-col'
          }
        },
        to: {
          true: {
            name: [
              'text-default peer-hover:text-highlighted peer-focus-visible:text-highlighted',
              'transition-colors'
            ],
            description: [
              'peer-hover:text-toned peer-focus-visible:text-toned',
              'transition-colors'
            ],
            avatar: 'transform transition-transform duration-200 group-hover/user:scale-115 group-has-focus-visible/user:scale-115'
          },
          false: {
            name: 'text-highlighted',
            description: ''
          }
        },
        size: {
          '3xs': {
            root: 'gap-1',
            wrapper: 'flex items-center gap-1',
            name: 'text-xs',
            description: 'text-xs'
          },
          '2xs': {
            root: 'gap-1.5',
            wrapper: 'flex items-center gap-1.5',
            name: 'text-xs',
            description: 'text-xs'
          },
          xs: {
            root: 'gap-1.5',
            wrapper: 'flex items-center gap-1.5',
            name: 'text-xs',
            description: 'text-xs'
          },
          sm: {
            root: 'gap-2',
            name: 'text-xs',
            description: 'text-xs'
          },
          md: {
            root: 'gap-2',
            name: 'text-sm',
            description: 'text-xs'
          },
          lg: {
            root: 'gap-2.5',
            name: 'text-sm',
            description: 'text-sm'
          },
          xl: {
            root: 'gap-2.5',
            name: 'text-base',
            description: 'text-sm'
          },
          '2xl': {
            root: 'gap-3',
            name: 'text-base',
            description: 'text-base'
          },
          '3xl': {
            root: 'gap-3',
            name: 'text-lg',
            description: 'text-base'
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})
```

## Changelog

See commit history for \[component]\(https\://github.com/nuxt/ui/commits/v4/src/runtime/components/User.vue) and \[theme]\(https\://github.com/nuxt/ui/commits/v4/src/theme/user.ts).


# defineLocale

## Usage

Use the `defineLocale` utility to create a custom locale with your own translations.

```vue
<script setup lang="ts">
import type { Messages } from '@nuxt/ui'

const locale = defineLocale<Messages>({
  name: 'My custom locale',
  code: 'en',
  dir: 'ltr',
  messages: {
    // implement pairs
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!TIP]
\> See: /docs/getting-started/integrations/i18n
\> Learn more about internationalization in the i18n integration documentation.

## API

`defineLocale<M>(options: DefineLocaleOptions<M>): Locale<M>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Creates a new locale object with the provided options.

#### Parameters

The locale configuration object with the following properties:
The display name of the locale (e.g., , ).
The ISO code of the locale (e.g., , , ).
The text direction of the locale. Defaults to .
The translation messages object. Use the type from for type safety.

**Returns:** A `Locale<M>` object that can be passed to the `locale` prop of the [App](https://ui.nuxt.com/docs/components/app) component.

## Example

Here's a complete example of creating a custom locale:

```vue
<script setup lang="ts">
import type { Messages } from '@nuxt/ui'

const locale = defineLocale<Messages>({
  name: 'Español',
  code: 'es',
  dir: 'ltr',
  messages: {
    alert: {
      close: 'Cerrar'
    },
    modal: {
      close: 'Cerrar'
    },
    commandPalette: {
      back: 'Atrás',
      close: 'Cerrar',
      noData: 'Sin datos',
      noMatch: 'Sin resultados',
      placeholder: 'Escribe un comando o busca…'
    }
    // ... other component messages
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!NOTE]
\> You can look at the \[built-in locales]\(https\://github.com/nuxt/ui/tree/v4/src/runtime/locale) for reference on how to structure the messages object.


# defineShortcuts

## Usage

Use the auto-imported `defineShortcuts` composable to define keyboard shortcuts.

```vue
<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  meta_k: () => {
    open.value = !open.value
  }
})
</script>
```

- Shortcuts are automatically adjusted for non-macOS platforms, converting `meta` to `ctrl`.
- The composable uses VueUse's [`useEventListener`](https://vueuse.org/core/useEventListener/){rel="&#x22;nofollow&#x22;"} to handle keydown events.
- For a complete list of available shortcut keys, refer to the [`KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values){rel="&#x22;nofollow&#x22;"} API documentation. Note that the key should be written in lowercase.

\> \[!TIP]
\> See: /docs/components/kbd
\> Learn how to display shortcuts in components in the Kbd component documentation.

## API

`defineShortcuts(config: ShortcutsConfig, options?: ShortcutsOptions): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Define keyboard shortcuts for your application.

#### Parameters

An object where keys are shortcut definitions and values are either handler functions or shortcut configuration objects.
Optional configuration for the shortcuts behavior.
The delay between key presses to consider the shortcut as chained. Default is .
When enabled, shortcuts work consistently across different keyboard layouts (Arabic, Hebrew) by matching physical key positions rather than character values.
false (default): Uses e.key for character-based matching (Layout specific)true: Uses e.code for physical key matching (Layout agnostic)

#### Shortcut definition

Shortcuts are defined using the following format:

- Single key: `'a'`, `'b'`, `'1'`, `'?'`, etc.
- Key combinations: Use `_` to separate keys, e.g., `'meta_k'`, `'ctrl_shift_f'`
- Key sequences: Use `-` to define a sequence, e.g., `'g-d'`

#### Modifiers

- `meta`: Represents `⌘ Command` on macOS and `Ctrl` on other platforms
- `ctrl`: Represents `Ctrl` on all platforms
- `shift`: Used for alphabetic keys when Shift is required

#### Special keys

- `escape`: Triggers on Esc key
- `enter`: Triggers on Enter key
- `arrowleft`, `arrowright`, `arrowup`, `arrowdown`: Trigger on respective arrow keys

#### Shortcut configuration

Each shortcut can be defined as a function or an object with the following properties:

`interface ShortcutConfig { handler: () => void; usingInput?: boolean | string }`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

#### Parameters

Function to be executed when the shortcut is triggered.
Controls when the shortcut should trigger based on input focus:
false (default): Shortcut only triggers when no input is focusedtrue: Shortcut triggers even when any input is focusedstring: Shortcut only triggers when the specified input (by name) is focused

## Examples

### Basic usage

```vue
<script setup lang="ts">
defineShortcuts({
  '?': () => openHelpModal(),
  'meta_k': () => openCommandPalette(),
  'g-d': () => navigateToDashboard()
})
</script>
```

### With input focus handling

The `usingInput` option allows you to specify that a shortcut should only trigger when a specific input is focused.

```vue
<template>
  <UInput v-model="query" name="queryInput" />
</template>

<script setup lang="ts">
const query = ref('')

defineShortcuts({
  enter: {
    usingInput: 'queryInput',
    handler: () => performSearch()
  },
  escape: {
    usingInput: true,
    handler: () => clearSearch()
  }
})
</script>
```

### Extracting shortcuts from menu items

Use the `extractShortcuts` utility to automatically define shortcuts from menu items.

\> \[!TIP]
\> See: /docs/composables/extract-shortcuts
\> Learn more about the extractShortcuts utility.


# extendLocale

## Usage

Use the `extendLocale` utility to customize an existing locale by overriding specific properties or messages.

```vue
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'

const locale = extendLocale(en, {
  code: 'en-AU',
  messages: {
    commandPalette: {
      placeholder: 'Search a component...'
    }
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

This is useful when you want to:

- Create a regional variant of a language (e.g., `en-AU` from `en`)
- Override specific translations without redefining the entire locale
- Customize component labels for your application

\> \[!TIP]
\> See: /docs/getting-started/integrations/i18n
\> Learn more about internationalization in the i18n integration documentation.

## API

`extendLocale<M>(locale: Locale<M>, options: Partial<DefineLocaleOptions<DeepPartial<M>>>): Locale<M>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Extends an existing locale with the provided options, deeply merging the messages.

#### Parameters

The base locale to extend. Import from .
The properties to override:
Override the display name of the locale.
Override the ISO code of the locale (e.g., , ).
Override the text direction of the locale.
Partial messages object to merge with the base locale. Only specify the messages you want to override.

**Returns:** A new `Locale<M>` object with the merged properties.

## Example

Here's an example extending the English locale for an Australian variant:

```vue
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'

const locale = extendLocale(en, {
  name: 'English (Australia)',
  code: 'en-AU',
  messages: {
    colorMode: {
      dark: 'Dark',
      light: 'Light',
      system: 'System'
    },
    selectMenu: {
      search: 'Search…',
      noData: 'No results found',
      noMatch: 'No matching results'
    }
  }
})
</script>

<template>
  <UApp :locale="locale">
    <NuxtPage />
  </UApp>
</template>
```

\> \[!NOTE]
\> The \`extendLocale\` utility uses deep merging, so you only need to specify the messages you want to override. All other messages will be inherited from the base locale.


# extractShortcuts

## Usage

Use the auto-imported `extractShortcuts` utility to define keyboard shortcuts from menu items. It extracts shortcuts from components like [DropdownMenu](https://ui.nuxt.com/docs/components/dropdown-menu), [ContextMenu](https://ui.nuxt.com/docs/components/context-menu) or [CommandPalette](https://ui.nuxt.com/docs/components/command-palette) where items have `kbds` defined.

```vue
<script setup lang="ts">
const items = [{
  label: 'Save',
  icon: 'i-lucide-file-down',
  kbds: ['meta', 'S'],
  onSelect() {
    save()
  }
}, {
  label: 'Copy',
  icon: 'i-lucide-copy',
  kbds: ['meta', 'C'],
  onSelect() {
    copy()
  }
}]

defineShortcuts(extractShortcuts(items))
</script>
```

\> \[!TIP]
\> See: /docs/composables/define-shortcuts
\> Learn more about keyboard shortcuts in the defineShortcuts composable documentation.

## API

`extractShortcuts(items: any[] | any[][], separator?: '_' | '-'): ShortcutsConfig`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Extracts keyboard shortcuts from an array of menu items and returns a configuration object compatible with `defineShortcuts`.

#### Parameters

An array of menu items (or nested arrays) containing shortcut definitions. Each item can have the following properties:
An array of keyboard keys that form the shortcut (e.g., ).
A callback function to execute when the shortcut is triggered.
An alternative callback function (used if is not defined).
Nested menu items to recursively extract shortcuts from.
Alternative property for nested menu items.
The separator used to join keyboard keys. Use for key combinations (e.g., ) or for key sequences (e.g., ). Defaults to .

**Returns:** A `ShortcutsConfig` object that can be passed directly to `defineShortcuts`.

## Examples

### With nested items

The utility recursively traverses `children` and `items` properties to extract shortcuts from nested menu structures.

```vue
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'

const items: DropdownMenuItem[][] = [[{
  label: 'Edit',
  icon: 'i-lucide-pencil',
  kbds: ['E'],
  onSelect() {
    edit()
  }
}, {
  label: 'Duplicate',
  icon: 'i-lucide-copy',
  kbds: ['D'],
  onSelect() {
    duplicate()
  }
}], [{
  label: 'Invite users',
  icon: 'i-lucide-user-plus',
  children: [[{
    label: 'Invite by email',
    icon: 'i-lucide-send-horizontal',
    kbds: ['meta', 'E'],
    onSelect() {
      inviteByEmail()
    }
  }, {
    label: 'Invite by link',
    icon: 'i-lucide-link',
    kbds: ['meta', 'I'],
    onSelect() {
      inviteByLink()
    }
  }]]
}], [{
  label: 'Delete',
  icon: 'i-lucide-trash',
  kbds: ['meta', 'backspace'],
  onSelect() {
    remove()
  }
}]]

defineShortcuts(extractShortcuts(items))
</script>

<template>
  <UDropdownMenu :items="items">
    <UButton label="Actions" />
  </UDropdownMenu>
</template>
```

### With key sequences

Use the `separator` parameter to create key sequences instead of key combinations.

```vue
<script setup lang="ts">
const items = [{
  label: 'Go to Dashboard',
  kbds: ['G', 'D'],
  onSelect() {
    navigateTo('/dashboard')
  }
}, {
  label: 'Go to Settings',
  kbds: ['G', 'S'],
  onSelect() {
    navigateTo('/settings')
  }
}]

// Using '-' creates key sequences: 'g-d', 'g-s'
defineShortcuts(extractShortcuts(items, '-'))
</script>
```


# useFormField

## Usage

Use the auto-imported `useFormField` composable to integrate custom inputs with a [Form](https://ui.nuxt.com/docs/components/form).

```vue
<script setup lang="ts">
const { id, emitFormBlur, emitFormInput, emitFormChange } = useFormField()
</script>
```


# useOverlay

## Usage

Use the auto-imported `useOverlay` composable to programmatically control [Modal](https://ui.nuxt.com/docs/components/modal) and [Slideover](https://ui.nuxt.com/docs/components/slideover) components.

```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'

const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)

async function openModal() {
  modal.open()
}
</script>
```

- The `useOverlay` composable is created using `createSharedComposable`, ensuring that the same overlay state is shared across your entire application.

\> \[!NOTE]
\> In order to return a value from the overlay, the \`overlay.open()\` can be awaited. In order for this to work, however, the overlay component must emit a \`close\` event. See example below for details.

## API

`useOverlay()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

The `useOverlay` composable provides methods to manage overlays globally. Each created overlay returns an instance with its own methods.

### create()

`create(component: T, options: OverlayOptions): OverlayInstance`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Create an overlay, and return a factory instance.

#### Parameters

The overlay component to render.
Configuration options for the overlay.
Open the overlay immediately after being created. Defaults to .
An optional object of props to pass to the rendered component.
Removes the overlay from memory when closed. Defaults to .

### open()

`open(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Open an overlay by its `id`.

#### Parameters

The identifier of the overlay.
An optional object of props to pass to the rendered component.

### close()

`close(id: symbol, value?: any): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Close an overlay by its `id`.

#### Parameters

The identifier of the overlay.
A value to resolve the overlay promise with.

### closeAll()

`closeAll(): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Close all open overlays.

### patch()

`patch(id: symbol, props: Partial<ComponentProps<T>>): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Update an overlay by its `id`.

#### Parameters

The identifier of the overlay.
An object of props to update on the rendered component.

### unmount()

`unmount(id: symbol): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Remove an overlay from the DOM by its `id`.

#### Parameters

The identifier of the overlay.

### isOpen()

`isOpen(id: symbol): boolean`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Check if an overlay is open using its `id`.

#### Parameters

The identifier of the overlay.

### overlays

`overlays: Overlay[]`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

In-memory list of all overlays that were created.

## Instance API

### open()

`open(props?: ComponentProps<T>): OpenedOverlay<T>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Open the overlay. Returns an `OpenedOverlay` which is a Promise that resolves with the value emitted by the `close` event.

#### Parameters

An optional object of props to pass to the rendered component.

```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'

const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)

function openModal() {
  modal.open({
    title: 'Welcome'
  })
}
</script>
```

### close()

`close(value?: any): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Close the overlay.

#### Parameters

A value to resolve the overlay promise with.

### patch()

`patch(props: Partial<ComponentProps<T>>): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Update the props of the overlay.

#### Parameters

An object of props to update on the rendered component.

```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'

const overlay = useOverlay()

const modal = overlay.create(LazyModalExample, {
  props: { title: 'Welcome' }
})

function openModal() {
  modal.open()
}

function updateModalTitle() {
  modal.patch({ title: 'Updated Title' })
}
</script>
```

## Examples

### With multiple overlays

This example demonstrates how to manage multiple overlays and pass data between them:

```vue
<script setup lang="ts">
import { ModalA, ModalB, SlideoverA } from '#components'

const overlay = useOverlay()

// Create with default props
const modalA = overlay.create(ModalA, { props: { title: 'Welcome' } })
const modalB = overlay.create(ModalB)
const slideoverA = overlay.create(SlideoverA)

const openModalA = () => {
  // Open modalA, but override the title prop
  modalA.open({ title: 'Hello' })
}

const openModalB = async () => {
  // Open modalB, and wait for its result
  const input = await modalB.open()

  // Pass the result from modalB to the slideover, and open it
  slideoverA.open({ input })
}
</script>

<template>
  <UButton label="Open Modal" @click="openModalA" />
</template>
```

### Confirm dialog

This example demonstrates how to create a reusable confirm dialog pattern using a custom `useConfirmDialog` composable that wraps `useOverlay`. This approach enables opinionated dialogs tailored to specific business requirements and design preferences.

1. Create a `ConfirmDialog` component that emits a boolean value when closed:

```vue [components/ConfirmDialog.vue]
<script lang="ts" setup>
interface ConfirmDialogProps {
  title?: string
  description?: string
}

defineProps<ConfirmDialogProps>()

const emits = defineEmits<{
  close: [value: boolean]
}>()
</script>

<template>
  <UModal
    :title="title"
    :description="description"
    :dismissible="false"
    :ui="{ footer: 'justify-end' }"
  >
    <template #footer>
      <UButton label="Cancel" color="neutral" variant="outline" @click="emits('close', false)" />
      <UButton label="Confirm" color="neutral" @click="emits('close', true)" />
    </template>
  </UModal>
</template>
```

2. Create a `useConfirmDialog` composable that returns a Promise:

```ts [composables/useConfirmDialog.ts]
import { ConfirmDialog } from '#components'

export interface ConfirmDialogOptions {
  title: string
  description?: string
}

export const useConfirmDialog = () => {
  const overlay = useOverlay()

  return (options: ConfirmDialogOptions): Promise<boolean> => {
    const modal = overlay.create(ConfirmDialog, {
      destroyOnClose: true,
      props: options
    })

    return modal.open()
  }
}
```

3. Use the composable in your components:

```vue
<script setup lang="ts">
const confirm = useConfirmDialog()

const handleDelete = async () => {
  const confirmed = await confirm({
    title: 'Delete item',
    description: 'Are you sure you want to delete this item?'
  })

  if (confirmed) {
    console.log('Item deleted')
  }
}
</script>

<template>
  <UButton label="Delete item" @click="handleDelete" />
</template>
```

## Caveats

### Provide / Inject

When opening overlays programmatically (e.g. modals, slideovers, etc), the overlay component can only access injected values from the component containing `UApp` (typically `app.vue` or layout components). This is because overlays are mounted outside of the page context by the `UApp` component.

As such, using `provide()` in pages or parent components isn't supported directly. To pass provided values to overlays, the recommended approach is to use props instead:

```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'

const overlay = useOverlay()

const providedValue = inject('valueProvidedInPage')

const modal = overlay.create(LazyModalExample, {
  props: {
    providedValue
  }
})
</script>
```


# useScrollShadow

## Usage

Use the auto-imported `useScrollShadow` composable to apply fade shadows on the edges of a scrollable element, indicating that more content is available in the scroll direction.

```vue
<script setup lang="ts">
const el = useTemplateRef('el')

const { style } = useScrollShadow(el)
</script>

<template>
  <div ref="el" class="max-h-[200px] overflow-y-auto" :style="style">
    <!-- Scrollable content -->
  </div>
</template>
```

- Uses CSS `mask-image` to fade content at the edges rather than overlay elements, so it works on any background.
- Automatically detects whether the element is overflowing and only applies shadows when needed.
- Supports both vertical and horizontal orientations.

## API

`useScrollShadow(element, options?)`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

### Parameters

A template ref or reactive reference to the scrollable element.
Configuration options for the scroll shadow.
The shadow size in pixels.
The scroll direction to apply shadows.

### Return

A reactive style object to bind on the scrollable element with . Contains when shadows are active, otherwise.
Whether the element's content overflows its visible area.
Reactive scroll arrival state from .

## Examples

### Horizontal

Use the `orientation` option for horizontally scrollable containers:

```vue
<script setup lang="ts">
const el = useTemplateRef('el')

const { style } = useScrollShadow(el, { orientation: 'horizontal' })
</script>

<template>
  <div ref="el" class="overflow-x-auto whitespace-nowrap" :style="style">
    <!-- Horizontally scrollable content -->
  </div>
</template>
```

### Custom size

Use the `size` option to change the shadow size in pixels:

```vue
<script setup lang="ts">
const el = useTemplateRef('el')

const { style } = useScrollShadow(el, { size: 48 })
</script>

<template>
  <div ref="el" class="max-h-[300px] overflow-y-auto" :style="style">
    <!-- Scrollable content -->
  </div>
</template>
```


# useToast

## Usage

Use the auto-imported `useToast` composable to display [Toast](https://ui.nuxt.com/docs/components/toast) notifications.

```vue
<script setup lang="ts">
const toast = useToast()
</script>
```

- The `useToast` composable uses Nuxt's `useState` to manage the toast state, ensuring reactivity across your application.
- A maximum of 5 toasts are displayed at a time. When adding a new toast that would exceed this limit, the oldest toast is automatically removed.
- When removing a toast, there's a 200ms delay before it's actually removed from the state, allowing for exit animations.

\> \[!WARNING]
\> Make sure to wrap your app with the \[\`App\`]\(/docs/components/app) component which uses our \[\`Toaster\`]\(https\://github.com/nuxt/ui/blob/v4/src/runtime/components/Toaster.vue) component which uses the \[\`ToastProvider\`]\(https\://reka-ui.com/docs/components/toast#provider) component from Reka UI.

\> \[!TIP]
\> See: /docs/components/toast
\> Learn how to customize the appearance and behavior of toasts in the Toast component documentation.

## API

`useToast()`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

The `useToast` composable provides methods to manage toast notifications globally.

### add()

`add(toast: Partial<Toast>): Toast`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Adds a new toast notification.

#### Parameters

A partial object with the following properties:
A unique identifier for the toast. If not provided, a timestamp will be used.
Whether the toast is open. Defaults to .
The title displayed in the toast.
The description displayed in the toast.
The icon displayed in the toast.
The avatar displayed in the toast. See .
The color of the toast.
The orientation between the content and the actions. Defaults to .
Customize or hide the close button (with value). Defaults to .
The icon displayed in the close button.
The actions displayed in the toast. See .
Customize or hide the progress bar (with value). Defaults to .
The duration in milliseconds before the toast auto-closes. Can also be set globally on the component.
A callback function invoked when the toast is clicked.
A callback function invoked when the toast open state changes. Useful to perform an action when the toast closes (expired or dismissed).

**Returns:** The complete `Toast` object that was added.

```vue
<script setup lang="ts">
const toast = useToast()

function showToast() {
  toast.add({
    title: 'Success',
    description: 'Your action was completed successfully.',
    color: 'success'
  })
}
</script>
```

### update()

`update(id: string | number, toast: Partial<Toast>): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Updates an existing toast notification.

#### Parameters

The unique identifier of the toast to update.
A partial object with the properties to update.

```vue
<script setup lang="ts">
const toast = useToast()

function updateToast(id: string | number) {
  toast.update(id, {
    title: 'Updated Toast',
    description: 'This toast has been updated.'
  })
}
</script>
```

### remove()

`remove(id: string | number): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Removes a toast notification.

#### Parameters

The unique identifier of the toast to remove.

```vue
<script setup lang="ts">
const toast = useToast()

function removeToast(id: string | number) {
  toast.remove(id)
}
</script>
```

### clear()

`clear(): void`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

Removes all toast notifications.

```vue
<script setup lang="ts">
const toast = useToast()

function clearAllToasts() {
  toast.clear()
}
</script>
```

### toasts

`toasts: Ref<Toast[]>`{.shiki,shiki-themes,material-theme-lighter,material-theme,material-theme-palenight lang="ts-type"}

A reactive array containing all current toast notifications.

```vue
<script setup lang="ts">
const { toasts } = useToast()
</script>

<template>
  <div>
    <pre>{{ toasts }}</pre>
  </div>
</template>
```
