GitHub repository

SearchPalette

Cmd/Ctrl+K search & command palette.

A keyboard-first search palette: a modal combobox over a grouped listbox, built on the native `<dialog>` like Modal. Bind `open` (the default `mod+k` shortcut toggles it globally), hand it `items`, and it filters, groups and keyboard-navigates them — focus stays on the input the whole time (`aria-activedescendant` pattern). Items with `href` render as real anchors, so Enter and click take the exact same native navigation path your router already intercepts. For async/external search, set `filter={false}` and bind `query`: the palette renders exactly the pre-filtered `items` you pass.

Demo

live · storybook Open in Storybook

Demo loads from https://dssoca-storybook.vercel.app/. Run pnpm storybook for it to render locally.

Usage

svelte
<script>
  import { SearchPalette } from 'dssoca';
  let open = $state(false);
</script>

<!-- opens with mod+K — Cmd+K on macOS, Ctrl+K elsewhere (or set bind:open yourself) -->
<SearchPalette
  bind:open
  items={[
    { id: 'home', label: 'Home', href: '/', group: 'Pages' },
    { id: 'tokens', label: 'Tokens', href: '/tokens', group: 'Pages', keywords: ['colors'] },
    { id: 'theme', label: 'Toggle theme' },
  ]}
  onselect={(item) => item.id === 'theme' && toggleTheme()}
/>

Built on the native `<dialog>` (focus trap, Esc, top layer, `::backdrop`) with the ARIA APG combobox pattern: focus stays on the input and options are referenced via `aria-activedescendant` — never DOM-focused. ArrowUp/Down wrap, PageUp/PageDown jump to first/last (Home/End keep their native caret behavior), Tab is swallowed while open, disabled items are skipped. The component is generic (`T extends SearchPaletteItem`), so your extra item fields flow through `onselect` and the `item` snippet typed. The toggle chord is registry-backed (id `ss:search-palette`): it lists in ShortcutsHelp, obeys `shortcuts.setEnabled()` / `shortcuts.remap()`, and fires on the platform modifier only (Cmd+K on macOS, Ctrl+K elsewhere — remap to `'mod+k, ctrl+k'` if you want both). Anything else bound to `mod+k` (another palette, a Topbar with `onCommand`) collides deterministically — the last one mounted wins — so give the chord to exactly one (`shortcut={false}` on the rest). Size controls the panel width only. Zero border-radius. This site's own ⌘K palette is this component — press `mod+k` or `/` anywhere in the docs to see it with a real index (pages, components with searchable props, actions).

Guide: Making your site keyboard-friendly.

HTML

CSS only — static markup The exact markup the component renders, generated at build time. Use it with dssoca/vanilla.css — see Plain HTML & CSS.

html
<dialog class="ss-search-palette" aria-label="Search">
  <div class="panel">
    <input value="" class="input" type="text" placeholder="Search…" spellcheck="false" autocomplete="off" role="combobox" aria-label="Search" aria-autocomplete="list" aria-expanded="true" aria-controls="search-palette-s1-listbox" aria-activedescendant="search-palette-s1-option-0"/>
    <div class="results">
      <ul id="search-palette-s1-listbox" class="listbox" role="listbox" aria-label="Search">
        <li class="group" role="presentation">
          <ul role="presentation">
            <li role="presentation">
              <a id="search-palette-s1-option-0" role="option" aria-selected="true" class="row active" href="/" tabindex="-1">
                <span class="label">Home</span>
              </a>
            </li>
          </ul>
        </li>
        <li class="group" role="group" aria-label="Pages">
          <span class="group-label" aria-hidden="true">Pages</span>
          <ul role="presentation">
            <li role="presentation">
              <a id="search-palette-s1-option-1" role="option" aria-selected="false" class="row" href="/logs" tabindex="-1">
                <span class="label">Logs</span>
              </a>
            </li>
          </ul>
        </li>
      </ul>
    </div>
    <footer class="foot">↑↓ navigate · ↵ open · esc close</footer>
  </div>
</dialog>

The combobox keyboard model and filtering need the Svelte component; the markup is shown for styling reference.

Props

PropTypeDefaultDescription
openbooleanfalseBindable. Whether the palette is shown; syncs to the native dialog.
itemsSearchPaletteItem[]—Required. Entries: `{ id, label, hint?, href?, group?, keywords?, disabled? }`. `href` items render as real `<a>`s (native routing); `group` buckets render in first-appearance order, ungrouped items first.
querystring''Bindable. The search text — bind it when you own filtering (`filter={false}`).
filterbooleantrueInternal case/diacritic-insensitive substring filter over `label` + `keywords`. Set false and pass pre-filtered `items` for external/async search.
shortcut'mod+k' | false'mod+k'Global toggle chord (Cmd+K on macOS, Ctrl+K elsewhere — the platform modifier only). Registered through the shortcut registry as `ss:search-palette`. `false` disables it.
placeholderstring'Search…'Input placeholder.
emptyTextstring'No results'No-results text (when the `empty` snippet is absent).
footerTextstring | false'↑↓ navigate · ↵ open · esc close'Footer hint row; `false` hides it. A `footer` snippet replaces it entirely.
resetOnClosebooleantrueClear the query and selection when the palette closes.
aria-labelstring'Search'Accessible name for the dialog, input and listbox.
onselect(item: T) => void | boolean—Called with the chosen item on Enter/click. Return `false` to keep the palette open.
onopen() => void—Called after the palette opens.
onclose() => void—Called after the palette closes (Esc, backdrop, or selection).
itemSnippet<[T, { active: boolean }]>—Replaces the default row body (label + hint). Must render non-interactive content — it lives inside `role="option"` and focus never leaves the input.
emptySnippet<[string]>—Replaces the default no-results state; receives the current query.
footerSnippet—Replaces the footer text row.
size'sm' | 'md' | 'lg'—Per-instance size override; inherits the ancestor `data-size-variant` when unset.
↑↓ navigate · ↵ open · esc close

Keyboard shortcuts

No shortcuts registered

Shortcuts added through the dssoca registry will show up here.

Single-key shortcuts