Command
Vue overlaySearchable command palette à la Cmd-K — keyboard-driven menu with grouped items, icons, shortcuts, and fuzzy filtering. Use as a global launcher (mounted in a Dialog) or inline as a typeahead select.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/command.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/command.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/command.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/command.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/command
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Dependencies
Used by
Files (10)
-
app/components/ui/command/Command.vue 2.3 kB
<script setup lang="ts"> import type { ListboxRootEmits, ListboxRootProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ListboxRoot, useFilter, useForwardPropsEmits } from 'reka-ui' import { reactive, ref, watch } from 'vue' import { cn } from '@/lib/utils' import { provideCommandContext } from '.' const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes['class'] }>(), { modelValue: '', }) const emits = defineEmits<ListboxRootEmits>() const delegatedProps = reactiveOmit(props, 'class') const forwarded = useForwardPropsEmits(delegatedProps, emits) const allItems = ref<Map<string, string>>(new Map()) const allGroups = ref<Map<string, Set<string>>>(new Map()) const { contains } = useFilter({ sensitivity: 'base' }) const filterState = reactive({ search: '', filtered: { /** The count of all visible items. */ count: 0, /** Map from visible item id to its search score. */ items: new Map() as Map<string, number>, /** Set of groups with at least one visible item. */ groups: new Set() as Set<string>, }, }) function filterItems() { if (!filterState.search) { filterState.filtered.count = allItems.value.size // Do nothing, each item will know to show itself because search is empty return } // Reset the groups filterState.filtered.groups = new Set() let itemCount = 0 // Check which items should be included for (const [id, value] of allItems.value) { const score = contains(value, filterState.search) filterState.filtered.items.set(id, score ? 1 : 0) if (score) itemCount++ } // Check which groups have at least 1 item shown for (const [groupId, group] of allGroups.value) { for (const itemId of group) { if (filterState.filtered.items.get(itemId)! > 0) { filterState.filtered.groups.add(groupId) break } } } filterState.filtered.count = itemCount } watch( () => filterState.search, () => { filterItems() }, ) provideCommandContext({ allItems, allGroups, filterState, }) </script> <template> <ListboxRoot data-uipkge data-slot="command" v-bind="forwarded" :class=" cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class) " > <slot /> </ListboxRoot> </template> -
app/components/ui/command/CommandDialog.vue 1 kB
<script setup lang="ts"> import type { DialogRootEmits, DialogRootProps } from 'reka-ui' import { useForwardPropsEmits } from 'reka-ui' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import Command from './Command.vue' const props = withDefaults( defineProps< DialogRootProps & { title?: string description?: string } >(), { title: 'Command Palette', description: 'Search for a command to run...', }, ) const emits = defineEmits<DialogRootEmits>() const forwarded = useForwardPropsEmits(props, emits) </script> <template> <Dialog v-slot="slotProps" v-bind="forwarded"> <DialogContent class="overflow-hidden p-0"> <DialogHeader class="sr-only"> <DialogTitle>{{ title }}</DialogTitle> <DialogDescription>{{ description }}</DialogDescription> </DialogHeader> <Command> <slot v-bind="slotProps" /> </Command> </DialogContent> </Dialog> </template> -
app/components/ui/command/CommandEmpty.vue 0.8 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Primitive } from 'reka-ui' import { computed } from 'vue' import { cn } from '@/lib/utils' import { useCommand } from '.' const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const { filterState } = useCommand() const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0) </script> <template> <Primitive v-if="isRender" data-uipkge data-slot="command-empty" v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)" > <slot /> </Primitive> </template> -
app/components/ui/command/CommandGroup.vue 1.4 kB
<script setup lang="ts"> import type { ListboxGroupProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ListboxGroup, ListboxGroupLabel, useId } from 'reka-ui' import { computed, onMounted, onUnmounted } from 'vue' import { cn } from '@/lib/utils' import { provideCommandGroupContext, useCommand } from '.' const props = defineProps< ListboxGroupProps & { class?: HTMLAttributes['class'] heading?: string } >() const delegatedProps = reactiveOmit(props, 'class') const { allGroups, filterState } = useCommand() const id = useId() const isRender = computed(() => (!filterState.search ? true : filterState.filtered.groups.has(id))) provideCommandGroupContext({ id }) onMounted(() => { if (!allGroups.value.has(id)) allGroups.value.set(id, new Set()) }) onUnmounted(() => { allGroups.value.delete(id) }) </script> <template> <ListboxGroup v-bind="delegatedProps" :id="id" data-uipkge data-slot="command-group" :class="cn('text-foreground overflow-hidden p-1', props.class)" :hidden="isRender ? undefined : true" > <ListboxGroupLabel v-if="heading" data-uipkge data-slot="command-group-heading" class="text-muted-foreground px-2 py-1.5 text-xs font-medium" > {{ heading }} </ListboxGroupLabel> <slot /> </ListboxGroup> </template> -
app/components/ui/command/CommandInput.vue 1.5 kB
<script setup lang="ts"> import type { ListboxFilterProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Search } from 'lucide-vue-next' import { ListboxFilter, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' import { useCommand } from '.' defineOptions({ inheritAttrs: false, }) const props = withDefaults( defineProps< ListboxFilterProps & { class?: HTMLAttributes['class'] } >(), // Focus on mount by default (command-palette behavior). Consumers embedding // Command inline on a page can pass :auto-focus="false" so it doesn't steal // focus and scroll the input into view on load. { autoFocus: true }, ) const delegatedProps = reactiveOmit(props, 'class') const forwardedProps = useForwardProps(delegatedProps) const { filterState } = useCommand() </script> <template> <div data-uipkge data-slot="command-input-wrapper" class="flex h-9 items-center gap-2 border-b px-3"> <Search class="text-muted-foreground size-4 shrink-0" aria-hidden="true" /> <ListboxFilter v-bind="{ ...forwardedProps, ...$attrs }" v-model="filterState.search" data-uipkge data-slot="command-input" :class=" cn( 'placeholder:text-muted-foreground focus-visible:ring-ring/40 flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', props.class, ) " /> </div> </template> -
app/components/ui/command/CommandItem.vue 2.4 kB
<script setup lang="ts"> import type { ListboxItemEmits, ListboxItemProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit, useCurrentElement } from '@vueuse/core' import { ListboxItem, useForwardPropsEmits, useId } from 'reka-ui' import { computed, onMounted, onUnmounted, ref } from 'vue' import { cn } from '@/lib/utils' import { useCommand, useCommandGroup } from '.' const props = defineProps<ListboxItemProps & { class?: HTMLAttributes['class'] }>() const emits = defineEmits<ListboxItemEmits>() const delegatedProps = reactiveOmit(props, 'class') const forwarded = useForwardPropsEmits(delegatedProps, emits) const id = useId() const { filterState, allItems, allGroups } = useCommand() const groupContext = useCommandGroup() const isRender = computed(() => { if (!filterState.search) { return true } else { const filteredCurrentItem = filterState.filtered.items.get(id) // If the filtered items is undefined means not in the all times map yet // Do the first render to add into the map if (filteredCurrentItem === undefined) { return true } // Check with filter return filteredCurrentItem > 0 } }) const itemRef = ref() const currentElement = useCurrentElement(itemRef) onMounted(() => { if (!(currentElement.value instanceof HTMLElement)) return // textValue to perform filter allItems.value.set(id, currentElement.value.textContent ?? props.value?.toString() ?? '') const groupId = groupContext?.id if (groupId) { if (!allGroups.value.has(groupId)) { allGroups.value.set(groupId, new Set([id])) } else { allGroups.value.get(groupId)?.add(id) } } }) onUnmounted(() => { allItems.value.delete(id) }) </script> <template> <ListboxItem v-if="isRender" v-bind="forwarded" :id="id" ref="itemRef" data-uipkge data-slot="command-item" :class=" cn( 'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class, ) " @select=" () => { filterState.search = '' } " > <slot /> </ListboxItem> </template> -
app/components/ui/command/CommandList.vue 0.7 kB
<script setup lang="ts"> import type { ListboxContentProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ListboxContent, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<ListboxContentProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const forwarded = useForwardProps(delegatedProps) </script> <template> <ListboxContent data-uipkge data-slot="command-list" v-bind="forwarded" :class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)" > <div role="presentation"> <slot /> </div> </ListboxContent> </template> -
app/components/ui/command/CommandSeparator.vue 0.6 kB
<script setup lang="ts"> import type { SeparatorProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Separator } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') </script> <template> <Separator data-uipkge data-slot="command-separator" v-bind="delegatedProps" :class="cn('bg-border -mx-1 h-px', props.class)" > <slot /> </Separator> </template> -
app/components/ui/command/CommandShortcut.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <span data-uipkge data-slot="command-shortcut" :class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)" > <slot /> </span> </template> -
app/components/ui/command/index.ts 1 kB
import type { Ref } from 'vue' import { createContext } from 'reka-ui' export { default as Command } from './Command.vue' export { default as CommandDialog } from './CommandDialog.vue' export { default as CommandEmpty } from './CommandEmpty.vue' export { default as CommandGroup } from './CommandGroup.vue' export { default as CommandInput } from './CommandInput.vue' export { default as CommandItem } from './CommandItem.vue' export { default as CommandList } from './CommandList.vue' export { default as CommandSeparator } from './CommandSeparator.vue' export { default as CommandShortcut } from './CommandShortcut.vue' export const [useCommand, provideCommandContext] = createContext<{ allItems: Ref<Map<string, string>> allGroups: Ref<Map<string, Set<string>>> filterState: { search: string filtered: { count: number; items: Map<string, number>; groups: Set<string> } } }>('Command') export const [useCommandGroup, provideCommandGroupContext] = createContext<{ id?: string }>('CommandGroup')
Raw manifest: https://uipkge.dev/r/vue/command.json