Lzh on GitHub

CommandPalette

一个由 Fuse.js 提供支持的命令面板,用于高效的模糊匹配。

用法

使用 v-model 指令控制 CommandPalette 的值,或者使用 default-value prop 在不需要控制其状态时设置初始值。

你也可以使用 @update:model-value 事件来监听选定的项目。

组 (Groups)

CommandPalette 组件根据用户输入筛选组并按相关性对匹配的命令进行排序。它提供动态、即时的搜索结果,以便高效地发现命令。使用 groups prop,它是一个包含以下属性的对象数组:

每个组必须提供一个 id,否则该组将被忽略。

每个组都包含一个 items 对象数组,用于定义命令。每个项目可以具有以下属性:

  • prefix?: string
  • label?: string
  • suffix?: string
  • icon?: string
  • avatar?: AvatarProps
  • chip?: ChipProps
  • kbds?: string[] | KbdProps[]
  • active?: boolean
  • loading?: boolean
  • disabled?: boolean
  • slot?: string
  • onSelect?(e?: Event): void
  • class?: any
  • 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,}

你可以传递 Link 组件的任何属性,例如 totarget 等。

<script setup lang="ts">
const groups = ref([
  {
    id: 'users',
    label: 'Users',
    items: [
      {
        label: 'Benjamin Canac',
        suffix: 'benjamincanac',
        avatar: {
          src: 'https://github.com/benjamincanac.png'
        }
      },
      {
        label: 'Sylvain Marroufin',
        suffix: 'smarroufin',
        avatar: {
          src: 'https://github.com/smarroufin.png'
        }
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        avatar: {
          src: 'https://github.com/atinux.png'
        }
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        avatar: {
          src: 'https://github.com/romhml.png'
        }
      },
      {
        label: 'Haytham A. Salama',
        suffix: 'Haythamasalama',
        avatar: {
          src: 'https://github.com/Haythamasalama.png'
        }
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        avatar: {
          src: 'https://github.com/danielroe.png'
        }
      },
      {
        label: 'Neil Richter',
        suffix: 'noook',
        avatar: {
          src: 'https://github.com/noook.png'
        }
      }
    ]
  }
])
const value = ref({})
</script>

<template>
  <UCommandPalette v-model="value" :groups="groups" class="flex-1" />
</template>

多选 (Multiple)

使用 multiple prop 允许选择多个项目。

<script setup lang="ts">
const groups = ref([
  {
    id: 'users',
    label: 'Users',
    items: [
      {
        label: 'Benjamin Canac',
        suffix: 'benjamincanac',
        avatar: {
          src: 'https://github.com/benjamincanac.png'
        }
      },
      {
        label: 'Sylvain Marroufin',
        suffix: 'smarroufin',
        avatar: {
          src: 'https://github.com/smarroufin.png'
        }
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        avatar: {
          src: 'https://github.com/atinux.png'
        }
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        avatar: {
          src: 'https://github.com/romhml.png'
        }
      },
      {
        label: 'Haytham A. Salama',
        suffix: 'Haythamasalama',
        avatar: {
          src: 'https://github.com/Haythamasalama.png'
        }
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        avatar: {
          src: 'https://github.com/danielroe.png'
        }
      },
      {
        label: 'Neil Richter',
        suffix: 'noook',
        avatar: {
          src: 'https://github.com/noook.png'
        }
      }
    ]
  }
])
const value = ref([])
</script>

<template>
  <UCommandPalette multiple v-model="value" :groups="groups" class="flex-1" />
</template>
确保将数组传递给 default-value prop 或 v-model 指令。

Placeholder

使用 placeholder prop 来更改占位符文本。

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette placeholder="Search an app..." :groups="groups" class="flex-1" />
</template>

Icon

使用 icon prop 来自定义输入框的 Icon。默认为 i-lucide-search

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette icon="i-lucide-box" :groups="groups" class="flex-1" />
</template>
你可以在 app.config.ts 中通过 ui.icons.search 键全局自定义此图标。
你可以在 vite.config.ts 中通过 ui.icons.search 键全局自定义此图标。

Selected Icon

使用 selected-icon 属性来自定义已选项的 Icon。默认值为 i-lucide-check

<script setup lang="ts">
const value = ref([
  {
    label: 'Benjamin Canac',
    suffix: 'benjamincanac',
    avatar: {
      src: 'https://github.com/benjamincanac.png'
    }
  }
])
const groups = ref([
  {
    id: 'users',
    label: 'Users',
    items: [
      {
        label: 'Benjamin Canac',
        suffix: 'benjamincanac',
        avatar: {
          src: 'https://github.com/benjamincanac.png'
        }
      },
      {
        label: 'Sylvain Marroufin',
        suffix: 'smarroufin',
        avatar: {
          src: 'https://github.com/smarroufin.png'
        }
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        avatar: {
          src: 'https://github.com/atinux.png'
        }
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        avatar: {
          src: 'https://github.com/romhml.png'
        }
      },
      {
        label: 'Haytham A. Salama',
        suffix: 'Haythamasalama',
        avatar: {
          src: 'https://github.com/Haythamasalama.png'
        }
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        avatar: {
          src: 'https://github.com/danielroe.png'
        }
      },
      {
        label: 'Neil Richter',
        suffix: 'noook',
        avatar: {
          src: 'https://github.com/noook.png'
        }
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette multiple selected-icon="i-lucide-circle-check" v-model="value" :groups="groups" class="flex-1" />
</template>
你可以在 vite.config.ts 中通过 ui.icons.check 键全局自定义此图标。

Trailing Icon

使用 trailing-icon 属性来自定义当项目有子项时的尾随图标。默认值为 i-lucide-chevron-right

<script setup lang="ts">
const groups = ref([
  {
    id: 'actions',
    items: [
      {
        label: 'Share',
        icon: 'i-lucide-share',
        children: [
          {
            label: 'Email',
            icon: 'i-lucide-mail'
          },
          {
            label: 'Copy',
            icon: 'i-lucide-copy'
          },
          {
            label: 'Link',
            icon: 'i-lucide-link'
          }
        ]
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette trailing-icon="i-lucide-arrow-right" :groups="groups" class="flex-1" />
</template>
你可以在 app.config.ts 中通过 ui.icons.chevronRight 键全局自定义此图标。

Loading

使用 loading prop 在 CommandPalette 上显示加载图标。

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette loading :groups="groups" class="flex-1" />
</template>

Loading Icon

使用 loading-icon prop 来自定义加载图标。默认为 i-lucide-loader-circle

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette loading loading-icon="i-lucide-loader" :groups="groups" class="flex-1" />
</template>
你可以在 app.config.ts 中通过 ui.icons.loading 键全局自定义此图标。
你可以在 vite.config.ts 中通过 ui.icons.loading 键全局自定义此图标。

Disabled

使用 disabled prop 来禁用 CommandPalette。

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette disabled :groups="groups" class="flex-1" />
</template>

关闭 (Close)

使用 close prop 来显示一个 Button 来关闭 CommandPalette。

当点击关闭按钮时,会发出 update:open 事件。
<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette close :groups="groups" class="flex-1" />
</template>

你可以传递 Button 组件的任何属性来自定义它。

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
    :groups="groups"
    class="flex-1"
  />
</template>

关闭图标 (Close Icon)

使用 close-icon prop 来自定义关闭按钮的 Icon。默认为 i-lucide-x

<script setup lang="ts">
const groups = ref([
  {
    id: 'apps',
    items: [
      {
        label: 'Calendar',
        icon: 'i-lucide-calendar'
      },
      {
        label: 'Music',
        icon: 'i-lucide-music'
      },
      {
        label: 'Maps',
        icon: 'i-lucide-map'
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette close close-icon="i-lucide-arrow-right" :groups="groups" class="flex-1" />
</template>
你可以在 app.config.ts 中通过 ui.icons.close 键全局自定义此图标。
你可以在 vite.config.ts 中通过 ui.icons.close 键全局自定义此图标。

Back

使用 back 属性来自定义或隐藏(当值为 false 时)在导航到子菜单时显示的返回按钮。

你可以传递 Button 组件的任何属性来自定义它。

<script setup lang="ts">
const groups = ref([
  {
    id: 'actions',
    items: [
      {
        label: 'Share',
        icon: 'i-lucide-share',
        children: [
          {
            label: 'Email',
            icon: 'i-lucide-mail'
          },
          {
            label: 'Copy',
            icon: 'i-lucide-copy'
          },
          {
            label: 'Link',
            icon: 'i-lucide-link'
          }
        ]
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette :back="{
  color: 'primary'
}" :groups="groups" class="flex-1" />
</template>

Back Icon

使用 back-icon 属性来自定义返回按钮的图标。默认值为 i-lucide-arrow-left

<script setup lang="ts">
const groups = ref([
  {
    id: 'actions',
    items: [
      {
        label: 'Share',
        icon: 'i-lucide-share',
        children: [
          {
            label: 'Email',
            icon: 'i-lucide-mail'
          },
          {
            label: 'Copy',
            icon: 'i-lucide-copy'
          },
          {
            label: 'Link',
            icon: 'i-lucide-link'
          }
        ]
      }
    ]
  }
])
</script>

<template>
  <UCommandPalette back-icon="i-lucide-house" :groups="groups" class="flex-1" />
</template>
你可以在 vite.config.ts 中的 ui.icons.arrowLeft 键下全局自定义此图标。

示例

控制选定的项目

你可以通过使用 default-value prop 或 v-model 指令,通过在每个项目上使用 onSelect 字段,或者通过使用 @update:model-value 事件来控制选定的项目。

<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'
        }
      },
      {
        label: 'Sylvain Marroufin',
        suffix: 'smarroufin',
        to: 'https://github.com/smarroufin',
        target: '_blank',
        avatar: {
          src: 'https://github.com/smarroufin.png'
        }
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        to: 'https://github.com/atinux',
        target: '_blank',
        avatar: {
          src: 'https://github.com/atinux.png'
        }
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        to: 'https://github.com/romhml',
        target: '_blank',
        avatar: {
          src: 'https://github.com/romhml.png'
        }
      },
      {
        label: 'Haytham A. Salama',
        suffix: 'Haythamasalama',
        to: 'https://github.com/Haythamasalama',
        target: '_blank',
        avatar: {
          src: 'https://github.com/Haythamasalama.png'
        }
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        to: 'https://github.com/danielroe',
        target: '_blank',
        avatar: {
          src: 'https://github.com/danielroe.png'
        }
      },
      {
        label: 'Neil Richter',
        suffix: 'noook',
        to: 'https://github.com/noook',
        target: '_blank',
        avatar: {
          src: 'https://github.com/noook.png'
        }
      }
    ]
  },
  {
    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>

控制搜索词

使用 v-model:search-term 指令来控制搜索词。

<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'
    }
  },
  {
    label: 'Sylvain Marroufin',
    suffix: 'smarroufin',
    to: 'https://github.com/smarroufin',
    target: '_blank',
    avatar: {
      src: 'https://github.com/smarroufin.png'
    }
  },
  {
    label: 'Sébastien Chopin',
    suffix: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png'
    }
  },
  {
    label: 'Romain Hamel',
    suffix: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png'
    }
  },
  {
    label: 'Haytham A. Salama',
    suffix: 'Haythamasalama',
    to: 'https://github.com/Haythamasalama',
    target: '_blank',
    avatar: {
      src: 'https://github.com/Haythamasalama.png'
    }
  },
  {
    label: 'Daniel Roe',
    suffix: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png'
    }
  },
  {
    label: 'Neil Richter',
    suffix: 'noook',
    to: 'https://github.com/noook',
    target: '_blank',
    avatar: {
      src: 'https://github.com/noook.png'
    }
  }
]

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>
此示例使用 @update:model-value 事件在选择项目时重置搜索词。

使用 items 中的 children

你可以通过在 items 中使用 children 属性创建层级菜单。当某个项有 children 时,它会自动显示一个箭头图标并启用进入子菜单的导航。

<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>

使用获取的项

你可以从 API 获取项目并在 CommandPalette 中使用它们。

<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status } = await useFetch('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}` } })) || []
  },
  lazy: true
})

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'"
    :groups="groups"
    class="flex-1 h-80"
  />
</template>

使用忽略过滤器

你可以在组上将 ignoreFilter 字段设置为 true,以禁用内部搜索并使用你自己的搜索逻辑。

<script setup lang="ts">
import { refDebounced } from '@vueuse/core'

const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  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}` } })) || []
  },
  lazy: true
})

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'"
    :groups="groups"
    class="flex-1 h-80"
  />
</template>
此示例使用 refDebounced 来防抖 API 调用。

使用后置过滤的项

你可以在组上使用 postFilter 字段在搜索发生后过滤项目。

<script setup lang="ts">
const items = [
  {
    id: '/',
    label: 'Introduction',
    level: 1
  },
  {
    id: '/getting-started#whats-new-in-v3',
    label: 'What\'s new in v3?',
    level: 2
  },
  {
    id: '/getting-started#reka-ui',
    label: 'Reka UI',
    level: 3
  },
  {
    id: '/getting-started#tailwind-css-v4',
    label: 'Tailwind CSS v4',
    level: 3
  },
  {
    id: '/getting-started#tailwind-variants',
    label: 'Tailwind Variants',
    level: 3
  },
  {
    id: '/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>
Start typing to see items with higher level appear.

使用自定义 Fuse 搜索

你可以使用 fuse prop 来覆盖 useFuse 的选项,其默认值为:

{
  fuseOptions: {
    ignoreLocation: true,
    threshold: 0.1,
    keys: ['label', 'suffix']
  },
  resultLimit: 12,
  matchAllWhenSearchEmpty: true
}
fuseOptionsFuse.js 的选项,resultLimit 是返回的最大结果数量,matchAllWhenSearchEmpty 是一个布尔值,表示当搜索词为空时是否匹配所有项目。

例如,你可以设置 { fuseOptions: { includeMatches: true } } 来突出显示项目中的搜索词。

<script setup lang="ts">
const { data: users } = await useFetch('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}` } })) || []
  },
  lazy: true
})
</script>

<template>
  <UCommandPalette
    :groups="[{ id: 'users', items: users || [] }]"
    :fuse="{ fuseOptions: { includeMatches: true } }"
    class="flex-1 h-80"
  />
</template>

使用弹出框

你可以在 Popover 的内容中使用 CommandPalette 组件。

<script setup lang="ts">
const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error' as const
    }
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success' as const
    }
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info' as const
    }
  }
])
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>

使用模态框

你可以在 Modal 的内容中使用 CommandPalette 组件。

<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: '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}` } })) || []
  },
  lazy: true
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])
</script>

<template>
  <UModal>
    <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>

使用抽屉

你可以在 Drawer 的内容中使用 CommandPalette 组件。

<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: '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}` } })) || []
  },
  lazy: true
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])
</script>

<template>
  <UDrawer :handle="false">
    <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>

监听打开状态

当使用 close prop 时,你可以监听按钮点击时发出的 update:open 事件。

<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'
    }
  },
  {
    label: 'Sylvain Marroufin',
    suffix: 'smarroufin',
    to: 'https://github.com/smarroufin',
    target: '_blank',
    avatar: {
      src: 'https://github.com/smarroufin.png'
    }
  },
  {
    label: 'Sébastien Chopin',
    suffix: 'atinux',
    to: 'https://github.com/atinux',
    target: '_blank',
    avatar: {
      src: 'https://github.com/atinux.png'
    }
  },
  {
    label: 'Romain Hamel',
    suffix: 'romhml',
    to: 'https://github.com/romhml',
    target: '_blank',
    avatar: {
      src: 'https://github.com/romhml.png'
    }
  },
  {
    label: 'Haytham A. Salama',
    suffix: 'Haythamasalama',
    to: 'https://github.com/Haythamasalama',
    target: '_blank',
    avatar: {
      src: 'https://github.com/Haythamasalama.png'
    }
  },
  {
    label: 'Daniel Roe',
    suffix: 'danielroe',
    to: 'https://github.com/danielroe',
    target: '_blank',
    avatar: {
      src: 'https://github.com/danielroe.png'
    }
  },
  {
    label: 'Neil Richter',
    suffix: 'noook',
    to: 'https://github.com/noook',
    target: '_blank',
    avatar: {
      src: 'https://github.com/noook.png'
    }
  }
]
</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>
例如,当在 Modal 内部使用 CommandPalette 时,这会很有用。

使用 #footer 插槽在 CommandPalette 底部添加自定义内容,比如键盘快捷键帮助或其他操作。

<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>

使用自定义插槽

使用 slot 属性来自定义特定项目或组。

你将可以使用以下插槽:

  • #{{ item.slot }}
  • #{{ item.slot }}-leading
  • #{{ item.slot }}-label
  • #{{ item.slot }}-trailing
  • #{{ group.slot }}
  • #{{ group.slot }}-leading
  • #{{ group.slot }}-label
  • #{{ group.slot }}-trailing
<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: 'Sylvain Marroufin',
        suffix: 'smarroufin',
        to: 'https://github.com/smarroufin',
        target: '_blank'
      },
      {
        label: 'Sébastien Chopin',
        suffix: 'atinux',
        to: 'https://github.com/atinux',
        target: '_blank'
      },
      {
        label: 'Romain Hamel',
        suffix: 'romhml',
        to: 'https://github.com/romhml',
        target: '_blank'
      },
      {
        label: 'Haytham A. Salama',
        suffix: 'Haythamasalama',
        to: 'https://github.com/Haythamasalama',
        target: '_blank'
      },
      {
        label: 'Daniel Roe',
        suffix: 'danielroe',
        to: 'https://github.com/danielroe',
        target: '_blank'
      },
      {
        label: 'Neil Richter',
        suffix: 'noook',
        to: 'https://github.com/noook',
        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" />
    </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>
你也可以使用 #item, #item-leading, #item-label#item-trailing 插槽来自定义所有项目。

API

Props

Prop Default Type
as

'div'

any

The element or component this component should render as.

icon

appConfig.ui.icons.search

string

The icon displayed in the input.

selectedIcon

appConfig.ui.icons.check

string

The icon displayed when an item is selected.

trailingIcon

appConfig.ui.icons.chevronRight

string

The icon displayed when an item has children.

placeholder

t('commandPalette.placeholder')

string

The placeholder text for the input.

autofocus

true

boolean

Automatically focus the input when component is mounted.

close

false

boolean | Partial<ButtonProps>

Display a close button in the input (useful when inside a Modal for example). { size: 'md', color: 'neutral', variant: 'ghost' }

closeIcon

appConfig.ui.icons.close

string

The icon displayed in the close button.

back

true

boolean | ButtonProps

Display a button to navigate back in history. { size: 'md', color: 'neutral', variant: 'link' }

backIcon

appConfig.ui.icons.arrowLeft

string

The icon displayed in the back button.

groups

CommandPaletteGroup<CommandPaletteItem>[]

fuse

{ fuseOptions: { ignoreLocation: true, threshold: 0.1, keys: ['label', 'suffix'] }, resultLimit: 12, matchAllWhenSearchEmpty: true }

UseFuseOptions<CommandPaletteItem>

Options for useFuse.

labelKey

'label'

string

The key used to get the label from the item.

defaultValue

null | string | number | bigint | Record<string, any> | AcceptableValue[]

The value of the listbox when initially rendered. Use when you do not need to control the state of the Listbox

modelValue

''

null | string | number | bigint | Record<string, any> | AcceptableValue[]

The controlled value of the listbox. Can be binded with with v-model.

disabled

boolean

When true, prevents the user from interacting with listbox

multiple

boolean

Whether multiple options can be selected or not.

highlightOnHover

boolean

When true, hover over item will trigger highlight

selectionBehavior

'toggle'

"replace" | "toggle"

How multiple selection should behave in the collection.

loading

boolean

When true, the loading icon will be displayed.

loadingIcon

appConfig.ui.icons.loading

string

The icon when the loading prop is true.

searchTerm

string

ui

{ root?: ClassNameValue; input?: ClassNameValue; close?: ClassNameValue; back?: ClassNameValue; content?: ClassNameValue; ... 19 more ...; itemLabelSuffix?: ClassNameValue; }

Slots

Slot Type
empty

{ searchTerm?: string | undefined; }

footer

{ ui: { root: (props?: Record<string, any> | undefined) => string; input: (props?: Record<string, any> | undefined) => string; close: (props?: Record<string, any> | undefined) => string; ... 21 more ...; itemLabelSuffix: (props?: Record<...> | undefined) => string; }; }

back

{ ui: { root: (props?: Record<string, any> | undefined) => string; input: (props?: Record<string, any> | undefined) => string; close: (props?: Record<string, any> | undefined) => string; ... 21 more ...; itemLabelSuffix: (props?: Record<...> | undefined) => string; }; }

close

{ ui: { root: (props?: Record<string, any> | undefined) => string; input: (props?: Record<string, any> | undefined) => string; close: (props?: Record<string, any> | undefined) => string; ... 21 more ...; itemLabelSuffix: (props?: Record<...> | undefined) => string; }; }

item

{ item: CommandPaletteItem; index: number; }

item-leading

{ item: CommandPaletteItem; index: number; }

item-label

{ item: CommandPaletteItem; index: number; }

item-trailing

{ item: CommandPaletteItem; index: number; }

Emits

Event Type
update:open

boolean

update:modelValue

CommandPaletteItem

highlight

{ ref: HTMLElement; value: CommandPaletteItem; }

entryFocus

CustomEvent<any>

leave

Event

update:searchTerm

string

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    commandPalette: {
      slots: {
        root: 'flex flex-col min-h-0 min-w-0 divide-y divide-default',
        input: '[&>input]:h-12',
        close: '',
        back: 'p-0',
        content: 'relative overflow-hidden flex flex-col',
        footer: 'p-1',
        viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
        group: 'p-1 isolate',
        empty: 'py-6 text-center text-sm text-muted',
        label: 'p-1.5 text-xs font-semibold text-highlighted',
        item: 'group relative w-full flex items-center 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',
        itemLeadingIcon: 'shrink-0 size-5',
        itemLeadingAvatar: 'shrink-0',
        itemLeadingAvatarSize: '2xs',
        itemLeadingChip: 'shrink-0 size-5',
        itemLeadingChipSize: 'md',
        itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0 size-5',
        itemTrailingHighlightedIcon: 'shrink-0 size-5 text-dimmed hidden group-data-highlighted:inline-flex',
        itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
        itemTrailingKbdsSize: 'md',
        itemLabel: 'truncate space-x-1 text-dimmed',
        itemLabelBase: 'text-highlighted [&>mark]:text-inverted [&>mark]:bg-primary',
        itemLabelPrefix: 'text-default',
        itemLabelSuffix: 'text-dimmed [&>mark]:text-inverted [&>mark]:bg-primary'
      },
      variants: {
        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'
          }
        }
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        commandPalette: {
          slots: {
            root: 'flex flex-col min-h-0 min-w-0 divide-y divide-default',
            input: '[&>input]:h-12',
            close: '',
            back: 'p-0',
            content: 'relative overflow-hidden flex flex-col',
            footer: 'p-1',
            viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
            group: 'p-1 isolate',
            empty: 'py-6 text-center text-sm text-muted',
            label: 'p-1.5 text-xs font-semibold text-highlighted',
            item: 'group relative w-full flex items-center 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',
            itemLeadingIcon: 'shrink-0 size-5',
            itemLeadingAvatar: 'shrink-0',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'shrink-0 size-5',
            itemLeadingChipSize: 'md',
            itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
            itemTrailingIcon: 'shrink-0 size-5',
            itemTrailingHighlightedIcon: 'shrink-0 size-5 text-dimmed hidden group-data-highlighted:inline-flex',
            itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
            itemTrailingKbdsSize: 'md',
            itemLabel: 'truncate space-x-1 text-dimmed',
            itemLabelBase: 'text-highlighted [&>mark]:text-inverted [&>mark]:bg-primary',
            itemLabelPrefix: 'text-default',
            itemLabelSuffix: 'text-dimmed [&>mark]:text-inverted [&>mark]:bg-primary'
          },
          variants: {
            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'
              }
            }
          }
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'

export default defineConfig({
  plugins: [
    vue(),
    uiPro({
      ui: {
        commandPalette: {
          slots: {
            root: 'flex flex-col min-h-0 min-w-0 divide-y divide-default',
            input: '[&>input]:h-12',
            close: '',
            back: 'p-0',
            content: 'relative overflow-hidden flex flex-col',
            footer: 'p-1',
            viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
            group: 'p-1 isolate',
            empty: 'py-6 text-center text-sm text-muted',
            label: 'p-1.5 text-xs font-semibold text-highlighted',
            item: 'group relative w-full flex items-center 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',
            itemLeadingIcon: 'shrink-0 size-5',
            itemLeadingAvatar: 'shrink-0',
            itemLeadingAvatarSize: '2xs',
            itemLeadingChip: 'shrink-0 size-5',
            itemLeadingChipSize: 'md',
            itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
            itemTrailingIcon: 'shrink-0 size-5',
            itemTrailingHighlightedIcon: 'shrink-0 size-5 text-dimmed hidden group-data-highlighted:inline-flex',
            itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
            itemTrailingKbdsSize: 'md',
            itemLabel: 'truncate space-x-1 text-dimmed',
            itemLabelBase: 'text-highlighted [&>mark]:text-inverted [&>mark]:bg-primary',
            itemLabelPrefix: 'text-default',
            itemLabelSuffix: 'text-dimmed [&>mark]:text-inverted [&>mark]:bg-primary'
          },
          variants: {
            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'
              }
            }
          }
        }
      }
    })
  ]
})