Menu

Change language

GitHub repo

Command Palette

blocklayout

Header-grade search/command palette. Bundles the slim trigger button (with platform-aware ⌘K/Ctrl-K kbd hint) AND the modal CommandDialog into one block; consumers drop it once and the global keyboard shortcut wires itself. Takes a `groups` array of `{ heading, items: [{ label, hint, icon, onSelect }] }` and an `onSelect` callback. `showTrigger=false` hides the inline button so the consumer can fire it from elsewhere via `bind:open`.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/command-palette.json
Manifest:https://uipkge.dev/r/svelte/command-palette.jsonInstalls to:src/lib/components/blocks/command-palette/

Variants

Loading interactive previews…

npm dependencies

Includes

Used by

Files installed (1)

  • src/lib/components/blocks/command-palette/CommandPalette.svelte4.2 kB
    <script lang="ts" module>
      import type { Component } from 'svelte'
    
      export interface CommandPaletteItem {
        label: string
        value?: string
        hint?: string
        icon?: Component
        onSelect?: () => void
      }
    
      export interface CommandPaletteGroup {
        heading: string
        items: CommandPaletteItem[]
      }
    
      export interface CommandPaletteProps {
        groups?: CommandPaletteGroup[]
        placeholder?: string
        triggerLabel?: string
        showTrigger?: boolean
        /** Controlled open state (`bind:open`) — the Svelte counterpart of the Vue twin's exposed show()/hide()/toggle(). */
        open?: boolean
        onOpenChange?: (open: boolean) => void
        onSelect?: (item: CommandPaletteItem) => void
      }
    </script>
    
    <script lang="ts">
      import { onMount } from 'svelte'
      import { FileText, Inbox, KanbanSquare, LayoutDashboard, Search, Settings, Users } from '@lucide/svelte'
      import {
        CommandDialog,
        CommandEmpty,
        CommandGroup,
        CommandInput,
        CommandItem,
        CommandList,
        CommandSeparator,
        CommandShortcut,
      } from '$lib/components/ui/command'
    
      let {
        groups = [
          {
            heading: 'Navigate',
            items: [
              { label: 'Dashboard', hint: '/dashboard', icon: LayoutDashboard },
              { label: 'Inbox', hint: '/inbox', icon: Inbox },
              { label: 'Kanban', hint: '/kanban', icon: KanbanSquare },
              { label: 'Team', hint: '/team', icon: Users },
            ],
          },
          {
            heading: 'Settings',
            items: [
              { label: 'Profile', hint: '/profile', icon: FileText },
              { label: 'Settings', hint: '/settings', icon: Settings },
            ],
          },
        ],
        placeholder = 'Search pages, commands…',
        triggerLabel = 'Search pages, commands…',
        showTrigger = true,
        open = $bindable(false),
        onOpenChange,
        onSelect,
      }: CommandPaletteProps = $props()
    
      function setOpen(next: boolean) {
        open = next
        onOpenChange?.(next)
      }
    
      function pick(item: CommandPaletteItem) {
        setOpen(false)
        item.onSelect?.()
        onSelect?.(item)
      }
    
      function onKeydown(e: KeyboardEvent) {
        if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
          e.preventDefault()
          setOpen(!open)
        }
      }
    
      const triggerShortcut = $derived.by(() => {
        if (typeof navigator === 'undefined') return '⌘K'
        return /Mac|iPhone|iPad/i.test(navigator.platform) ? '⌘K' : 'Ctrl K'
      })
    
      onMount(() => {
        window.addEventListener('keydown', onKeydown)
        return () => window.removeEventListener('keydown', onKeydown)
      })
    </script>
    
    {#if showTrigger}
      <button
        data-slot="command-palette"
        type="button"
        class="bg-secondary/50 hover:bg-secondary text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 relative hidden h-8 w-full items-center gap-2 rounded-lg border border-transparent px-2.5 text-sm shadow-none transition-colors outline-none focus-visible:ring-[3px] sm:flex md:w-56 lg:w-80"
        aria-label="Open command palette"
        onclick={() => setOpen(true)}
      >
        <Search class="size-3.5 shrink-0" />
        <span class="flex-1 truncate text-left">{triggerLabel}</span>
        <kbd
          class="bg-muted/80 text-muted-foreground pointer-events-none flex h-5 items-center justify-center rounded-md border px-1.5 font-mono text-xs font-medium"
        >
          <span>{triggerShortcut}</span>
        </kbd>
      </button>
    {/if}
    
    <CommandDialog bind:open {onOpenChange} title="Command palette" description="Search pages and run commands">
      <CommandInput {placeholder} />
      <CommandList class="max-h-[30rem]">
        <CommandEmpty>No matches.</CommandEmpty>
        {#each groups as group, gi (group.heading)}
          <CommandGroup heading={group.heading}>
            {#each group.items as item (`${group.heading}-${item.label}`)}
              {@const ItemIcon = item.icon}
              <CommandItem value={`${group.heading} ${item.label} ${item.hint ?? ''}`} onSelect={() => pick(item)}>
                {#if ItemIcon}
                  <ItemIcon class="size-4" />
                {/if}
                <span>{item.label}</span>
                {#if item.hint}
                  <CommandShortcut class="text-muted-foreground/70">{item.hint}</CommandShortcut>
                {/if}
              </CommandItem>
            {/each}
          </CommandGroup>
          {#if gi < groups.length - 1}
            <CommandSeparator />
          {/if}
        {/each}
      </CommandList>
    </CommandDialog>
    

Raw manifest:https://uipkge.dev/r/svelte/command-palette.json