<script lang="ts">
import CalculatorIcon from "@lucide/svelte/icons/calculator";
import CalendarIcon from "@lucide/svelte/icons/calendar";
import CreditCardIcon from "@lucide/svelte/icons/credit-card";
import SettingsIcon from "@lucide/svelte/icons/settings";
import SmileIcon from "@lucide/svelte/icons/smile";
import UserIcon from "@lucide/svelte/icons/user";
import * as Command from "$lib/components/ui/command/index.js";
</script>
<Command.Root class="rounded-lg border shadow-md md:min-w-[450px]">
<Command.Input placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>
<CalendarIcon />
<span>Calendar</span>
</Command.Item>
<Command.Item>
<SmileIcon />
<span>Search Emoji</span>
</Command.Item>
<Command.Item disabled>
<CalculatorIcon />
<span>Calculator</span>
</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item>
<UserIcon />
<span>Profile</span>
<Command.Shortcut>⌘P</Command.Shortcut>
</Command.Item>
<Command.Item>
<CreditCardIcon />
<span>Billing</span>
<Command.Shortcut>⌘B</Command.Shortcut>
</Command.Item>
<Command.Item>
<SettingsIcon />
<span>Settings</span>
<Command.Shortcut>⌘S</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</Command.Root> Installation
pnpm dlx shadcn-svelte@latest add command Install bits-ui:
pnpm add bits-ui -D Copy and paste the following code into your project.
<script lang="ts">
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
import Command from './command.svelte';
import * as Dialog from '$UI$/dialog/index.js';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
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 border-[#222225] bg-[#09090b] p-0 shadow-none',
className
)}
{showCloseButton}
{portalProps}
>
<Command {...restProps} bind:value bind:ref {children} />
</Dialog.Content>
</Dialog.Root>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$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 font-mono text-xs uppercase tracking-[0.08em] text-zinc-500',
className
)}
{...restProps}
/>
<script lang="ts">
import { Command as CommandPrimitive, useId } from 'bits-ui';
import { cn } from '$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 p-1.5 text-zinc-50 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:font-mono **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:uppercase **:[[cmdk-group-heading]]:tracking-[0.08em] **:[[cmdk-group-heading]]:text-zinc-500',
className
)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading
class="px-2 py-1.5 font-mono text-xs font-medium uppercase tracking-[0.08em] text-zinc-500"
>
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
import * as InputGroup from '$UI$/input-group/index.js';
import MagnifyingGlassIcon from 'phosphor-svelte/lib/MagnifyingGlass';
let {
ref = $bindable(null),
class: className,
value = $bindable(''),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div data-slot="command-input-wrapper" class="border-b border-[#222225] bg-[#09090b] p-1">
<InputGroup.Root
class="border-transparent bg-[#18181b] px-3 shadow-[inset_0_0_0_1px_rgba(161,161,170,0.12)]"
>
<CommandPrimitive.Input
{value}
data-slot="command-input"
class={cn(
'w-full px-2 font-mono text-xs text-zinc-50 placeholder:text-zinc-500 outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...restProps}
>
{#snippet child({ props })}
<InputGroup.Input {...props} bind:value bind:ref />
{/snippet}
</CommandPrimitive.Input>
<InputGroup.Addon>
<MagnifyingGlassIcon class="size-3.5 shrink-0 text-[#d0e891] opacity-80" />
</InputGroup.Addon>
</InputGroup.Root>
</div>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
import CheckIcon from 'phosphor-svelte/lib/Check';
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 cursor-default items-center gap-2 border border-transparent px-2 py-1.5 font-mono text-xs uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none data-selected:border-[#b9d765]/50 data-selected:bg-[#18181b] data-selected:text-zinc-50 data-selected:*:[svg]:text-[#d0e891] data-[checked=true]:text-[#d0e891] in-data-[slot=dialog-content]:rounded-none! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{@render children?.()}
<CheckIcon
class="cn-command-item-indicator ml-auto text-[#d0e891] opacity-0 group-has-[[data-slot=command-shortcut]]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</CommandPrimitive.Item>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
bind:ref
data-slot="command-item"
class={cn(
"relative flex cursor-default items-center gap-2 border border-transparent px-2 py-1.5 font-mono text-xs uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none aria-selected:border-[#b9d765]/50 aria-selected:bg-[#18181b] aria-selected:text-zinc-50 [&_svg:not([class*='text-'])]:text-[#d0e891] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$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-72 scroll-py-1 overflow-x-hidden overflow-y-auto border-t border-[#222225] outline-none',
className
)}
{...restProps}
/>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LoadingProps = $props();
</script>
<CommandPrimitive.Loading
bind:ref
class={cn('px-3 py-2 font-mono text-xs uppercase tracking-[0.08em] text-zinc-500', className)}
{...restProps}
/>
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$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.5 h-px bg-[#222225]', className)}
{...restProps}
/>
<script lang="ts">
import { cn, type WithElementRef } from '$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-[11px] uppercase tracking-[0.12em] text-zinc-500 group-data-selected/command-item:text-[#d0e891]',
className
)}
{...restProps}
>
{@render children?.()}
</span>
<script lang="ts">
import { cn } from '$UTILS$.js';
import { Command as CommandPrimitive } from 'bits-ui';
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 border border-[#222225] bg-[#09090b] text-zinc-50 shadow-none',
className
)}
{...restProps}
/>
import Root from './command.svelte';
import Loading from './command-loading.svelte';
import Dialog from './command-dialog.svelte';
import Empty from './command-empty.svelte';
import Group from './command-group.svelte';
import Item from './command-item.svelte';
import Input from './command-input.svelte';
import List from './command-list.svelte';
import Separator from './command-separator.svelte';
import Shortcut from './command-shortcut.svelte';
import LinkItem from './command-link-item.svelte';
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading
};
Usage
<script lang="ts">
import * as Command from '$lib/components/ui/command/index.js';
</script> <Command.Root>
<Command.Input placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>Calendar</Command.Item>
<Command.Item>Search Emoji</Command.Item>
<Command.Item>Calculator</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item>Profile</Command.Item>
<Command.Item>Billing</Command.Item>
<Command.Item>Settings</Command.Item>
</Command.Group>
</Command.List>
</Command.Root> Examples
Dialog
Press ⌘J
Command Palette
Search for a command to run...
<script lang="ts">
import CalculatorIcon from "@lucide/svelte/icons/calculator";
import CalendarIcon from "@lucide/svelte/icons/calendar";
import CreditCardIcon from "@lucide/svelte/icons/credit-card";
import SettingsIcon from "@lucide/svelte/icons/settings";
import SmileIcon from "@lucide/svelte/icons/smile";
import UserIcon from "@lucide/svelte/icons/user";
import * as Command from "$lib/components/ui/command/index.js";
let open = $state(false);
function handleKeydown(e: KeyboardEvent) {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
open = !open;
}
}
</script>
<svelte:document onkeydown={handleKeydown} />
<p class="text-muted-foreground text-sm">
Press
<kbd
class="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none"
>
<span class="text-xs">⌘</span>J
</kbd>
</p>
<Command.Dialog bind:open>
<Command.Input placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>
<CalendarIcon class="me-2 size-4" />
<span>Calendar</span>
</Command.Item>
<Command.Item>
<SmileIcon class="me-2 size-4" />
<span>Search Emoji</span>
</Command.Item>
<Command.Item>
<CalculatorIcon class="me-2 size-4" />
<span>Calculator</span>
</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item>
<UserIcon class="me-2 size-4" />
<span>Profile</span>
<Command.Shortcut>⌘P</Command.Shortcut>
</Command.Item>
<Command.Item>
<CreditCardIcon class="me-2 size-4" />
<span>Billing</span>
<Command.Shortcut>⌘B</Command.Shortcut>
</Command.Item>
<Command.Item>
<SettingsIcon class="me-2 size-4" />
<span>Settings</span>
<Command.Shortcut>⌘S</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</Command.Dialog> To show the command menu in a dialog, use the <Command.Dialog /> component instead of <Command.Root />. It accepts props for both the <Dialog.Root /> and <Command.Root /> components.
<script lang="ts">
import * as Command from '$lib/components/ui/command/index.js';
import { onMount } from 'svelte';
let open = $state(false);
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
open = !open;
}
}
</script>
<svelte:document onkeydown={handleKeydown} />
<Command.Dialog bind:open>
<Command.Input placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>Calendar</Command.Item>
<Command.Item>Search Emoji</Command.Item>
<Command.Item>Calculator</Command.Item>
</Command.Group>
</Command.List>
</Command.Dialog> Changelog
2024-10-30 Classes for icons
- Added
gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0to the<Command.Item>component to automatically style the icons inside.