Skip to content
edmi-ui

Command

Fast, filterable command palette (⌘K) with groups, shortcuts and an empty state. Can open as a dialog from anywhere.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Command Palette

Search for a command to run...

Indexes
MAG4 · Magnificent Four
MAG7 · equal weight
Actions
Create index ⌘N
Ask agent ⌘J
Command Palette
Search for a command to run...
Terminal window
npx shadcn@latest add @edmi-ui/command
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/command
Terminal window
yarn dlx shadcn@latest add @edmi-ui/command
Terminal window
bunx shadcn@latest add @edmi-ui/command

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/command
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/command
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/command
Terminal window
bunx shadcn-vue@latest add @edmi-ui/command

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/command.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/command.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/command.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/command.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { Kbd } from "@/components/ui/kbd";
<div className="flex flex-col items-start gap-4">
<Command className="w-[420px] rounded-xl border border-border">
<CommandInput placeholder="Search indexes and actions…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Indexes">
<CommandItem>MAG4 · Magnificent Four</CommandItem>
<CommandItem>MAG7 · equal weight</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
<CommandItem>
<PlusIcon />{" "}
Create index <CommandShortcut>⌘N</CommandShortcut>
</CommandItem>
<CommandItem>
<SparkleIcon />{" "}
Ask agent <CommandShortcut>⌘J</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
<Button variant="outline">
Open palette <Kbd>⌘K</Kbd>
</Button>
{/* … */}
</div>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { Kbd } from "@/components/ui/kbd";
</script>
<template>
<div class="flex flex-col items-start gap-4">
<Command class="w-[420px] rounded-xl border border-border">
<CommandInput placeholder="Search indexes and actions…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Indexes">
<CommandItem value="mag4">MAG4 · Magnificent Four</CommandItem>
<CommandItem value="mag7">MAG7 · equal weight</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
<CommandItem value="create-index">
<PlusIcon />
Create index <CommandShortcut>⌘N</CommandShortcut>
</CommandItem>
<CommandItem value="ask-agent">
<SparklesIcon />
Ask agent <CommandShortcut>⌘J</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
<Button variant="outline">
Open palette <Kbd>⌘K</Kbd>
</Button>
<!-- … -->
</div>
</template>
<script lang="ts">
import * as Command from "$lib/components/ui/command";
</script>
<svelte:document onkeydown={onKeydown} />
<!-- … -->
registry/ui/command.tsx
"use client";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "cn";
import type * as React from "react";
import { MagnifyingGlassIcon, CheckIcon } from "@phosphor-icons/react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/registry/edmi/ui/dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
children: React.ReactNode;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden p-0 sm:max-w-xl",
className,
)}
showCloseButton={showCloseButton}
>
<Command className="rounded-none">{children}</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-12 items-center gap-2.5 border-b border-border px-3.5"
>
<MagnifyingGlassIcon
className="size-4 shrink-0 text-muted-foreground"
/>
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"h-full w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"no-scrollbar max-h-80 scroll-py-1.5 overflow-x-hidden overflow-y-auto p-1.5 outline-none",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:pt-1.5 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("-mx-1.5 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"group/command-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-45 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground",
className,
)}
{...props}
>
{children}
<CheckIcon
className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</CommandPrimitive.Item>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
};
registry/ui/command/Command.vue
<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 "@/registry/edmi/lib/utils"
import { provideCommandContext } from "."
const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(), {
modelValue: "",
highlightOnHover: true,
})
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-slot="command"
v-bind="forwarded"
:class="cn('flex size-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground', props.class)"
>
<slot />
</ListboxRoot>
</template>
registry/ui/command/CommandDialog.vue
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { useForwardPropsEmits } from "reka-ui"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/registry/edmi/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="top-1/3 translate-y-0 overflow-hidden p-0 sm:max-w-xl" :show-close-button="false">
<DialogHeader class="sr-only">
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<Command>
<slot v-bind="slotProps" />
</Command>
</DialogContent>
</Dialog>
</template>
registry/ui/command/CommandEmpty.vue
<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 "@/registry/edmi/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-slot="command-empty"
v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</Primitive>
</template>
registry/ui/command/CommandGroup.vue
<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 "@/registry/edmi/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-slot="command-group"
:class="cn('overflow-hidden text-foreground', props.class)"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground">
{{ heading }}
</ListboxGroupLabel>
<slot />
</ListboxGroup>
</template>
registry/ui/command/CommandInput.vue
<script setup lang="ts">
import type { ListboxFilterProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Search } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import { ListboxFilter, useForwardProps } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { useCommand } from "."
defineOptions({
inheritAttrs: false,
})
const props = defineProps<ListboxFilterProps & {
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
const { filterState } = useCommand()
</script>
<template>
<div
data-slot="command-input-wrapper"
class="flex h-12 items-center gap-2.5 border-b border-border px-3.5"
>
<Search class="size-4 shrink-0 text-muted-foreground" />
<ListboxFilter
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"
data-slot="command-input"
auto-focus
:class="cn('h-full w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>
registry/ui/command/CommandItem.vue
<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 "@/registry/edmi/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-slot="command-item"
:class="cn(`group/command-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground`, props.class)"
@select="() => {
filterState.search = ''
}"
>
<slot />
</ListboxItem>
</template>
registry/ui/command/CommandList.vue
<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 "@/registry/edmi/lib/utils"
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ListboxContent
data-slot="command-list"
v-bind="forwarded"
:class="cn('no-scrollbar max-h-80 scroll-py-1.5 overflow-x-hidden overflow-y-auto p-1.5 outline-none', props.class)"
>
<div role="presentation">
<slot />
</div>
</ListboxContent>
</template>
registry/ui/command/CommandSeparator.vue
<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 { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { useCommand } from "."
const props = defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
// Like cmdk: hide the separator while searching.
const { filterState } = useCommand()
const isRender = computed(() => !filterState.search)
</script>
<template>
<Separator
v-if="isRender"
data-slot="command-separator"
v-bind="delegatedProps"
:class="cn('-mx-1.5 my-1 h-px bg-border', props.class)"
>
<slot />
</Separator>
</template>
registry/ui/command/CommandShortcut.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="command-shortcut"
:class="cn('ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground', props.class)"
>
<slot />
</span>
</template>
registry/ui/command/index.ts
import { createContext } from "reka-ui";
import type { Ref } from "vue";
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");
src/lib/registry/ui/command/command-dialog.svelte
<script lang="ts">
import * as Dialog from "$lib/registry/ui/dialog/index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import Command from "./command.svelte";
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
import type { Snippet } from "svelte";
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(""),
title = "Command Palette",
description = "Search for a command to run...",
showCloseButton = false,
portalProps,
children,
class: className,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
title?: string;
description?: string;
showCloseButton?: boolean;
class?: string;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Header class="sr-only">
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content
class={cn("top-1/3 translate-y-0 overflow-hidden p-0 sm:max-w-xl", className)}
{showCloseButton}
{portalProps}
>
<Command {...restProps} bind:value bind:ref {children} />
</Dialog.Content>
</Dialog.Root>
src/lib/registry/ui/command/command-empty.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty
bind:ref
data-slot="command-empty"
class={cn(
"py-6 text-center text-sm", className)}
{...restProps}
/>
src/lib/registry/ui/command/command-group.svelte
<script lang="ts">
import { Command as CommandPrimitive, useId } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn(
"overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:pt-1.5 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:text-muted-foreground", className)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading class="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground">
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>
src/lib/registry/ui/command/command-input.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlass";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div
data-slot="command-input-wrapper"
class="flex h-12 items-center gap-2.5 border-b border-border px-3.5"
>
<MagnifyingGlassIcon
class="size-4 shrink-0 text-muted-foreground"
/>
<CommandPrimitive.Input
bind:ref
bind:value
data-slot="command-input"
class={cn(
"h-full w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
/>
</div>
src/lib/registry/ui/command/command-item.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import CheckIcon from "phosphor-svelte/lib/Check";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
bind:ref
data-slot="command-item"
class={cn(
"group/command-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[selected]:bg-accent data-[selected]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...restProps}
>
{@render children?.()}
<CheckIcon
class="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</CommandPrimitive.Item>
src/lib/registry/ui/command/command-link-item.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
bind:ref
data-slot="command-item"
class={cn(
"group/command-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[selected]:bg-accent data-[selected]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...restProps}
/>
src/lib/registry/ui/command/command-list.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn(
"no-scrollbar max-h-80 scroll-py-1.5 overflow-x-hidden overflow-y-auto p-1.5 outline-none", className)}
{...restProps}
/>
src/lib/registry/ui/command/command-loading.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
</script>
<CommandPrimitive.Loading bind:ref {...restProps} />
src/lib/registry/ui/command/command-separator.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn(
"-mx-1.5 my-1 h-px bg-border", className)}
{...restProps}
/>
src/lib/registry/ui/command/command-shortcut.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn(
"ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground", className)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ui/command/command.svelte
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
export type CommandRootApi = CommandPrimitive.Root;
let {
api = $bindable(null),
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: CommandPrimitive.RootProps & {
api?: CommandRootApi | null;
} = $props();
</script>
<CommandPrimitive.Root
bind:this={api}
bind:value
bind:ref
data-slot="command"
class={cn(
"flex size-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground", className)}
{...restProps}
/>
src/lib/registry/ui/command/index.ts
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Input from "./command-input.svelte";
import Item from "./command-item.svelte";
import LinkItem from "./command-link-item.svelte";
import List from "./command-list.svelte";
import Loading from "./command-loading.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
export {
Dialog,
Dialog as CommandDialog,
Empty,
Empty as CommandEmpty,
Group,
Group as CommandGroup,
Input,
Input as CommandInput,
Item,
Item as CommandItem,
LinkItem,
LinkItem as CommandLinkItem,
List,
List as CommandList,
Loading,
Loading as CommandLoading,
Root,
//
Root as Command,
Separator,
Separator as CommandSeparator,
Shortcut,
Shortcut as CommandShortcut,
};

Built on cmdk.

Prop Type Default
CommandDialog open / onOpenChange boolean / (open) => void none
CommandDialog title / description string screen-reader only
CommandDialog showCloseButton boolean false
CommandItem onSelect (value) => void none

Parts: Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator.

  • The input is a flat search row with a bottom border; the selected item uses --accent (cmdk data-[selected=true]).
  • Shortcuts are mono text. The dialog uses the flat Edmi dialog surface.