« home

svelte-multiselect Svelte MultiSelect

You can use <MultiSelect /> to build a navigation palette in just 70 lines of code (50 without styles).

<script>
  import { goto } from '$app/navigation'
  import { CmdPalette } from '$lib'

  const actions = Object.keys(import.meta.glob(`./**/+page.{svx,svelte,md}`)).map(
    (filename) => {
      const parts = filename.split(`/`).filter((part) => !part.startsWith(`(`)) // remove hidden route segments

      const route = `/${parts.slice(1, -1).join(`/`)}`

      return { label: route, action: () => goto(route) }
    }
  )
</script>

<CmdPalette {actions} />

Here’s <CmdPalette /> component

<script lang="ts">
  import { fade } from 'svelte/transition'
  import { MultiSelect } from '.'
  import type { MultiSelectProps, ObjectOption } from './types'

  interface Action extends ObjectOption {
    label: string
    action: (label: string) => void
  }

  interface Props extends Omit<MultiSelectProps<Action>, `options`> {
    actions: Action[]
    triggers?: string[]
    close_keys?: string[]
    fade_duration?: number // in ms
    dialog_style?: string // for dialog
    // for span in option snippet, has no effect when specifying a custom option snippet
    open?: boolean
    dialog?: HTMLDialogElement | null
    input?: HTMLInputElement | null
    placeholder?: string
  }
  let {
    actions,
    triggers = [`k`],
    close_keys = [`Escape`],
    fade_duration = 200,
    dialog_style = ``,
    open = $bindable(false),
    dialog = $bindable(null),
    input = $bindable(null),
    placeholder = `Filter actions...`,
    ...rest
  }: Props = $props()

  $effect(() => {
    if (open && input) input?.focus() // focus input when palette is opened
  })

  async function toggle(event: KeyboardEvent) {
    if (triggers.includes(event.key) && event.metaKey && !open) {
      open = true
    } else if (close_keys.includes(event.key) && open) {
      open = false
    }
  }

  function close_if_outside(event: MouseEvent) {
    if (open && !dialog?.contains(event.target as Node)) {
      open = false
    }
  }

  function trigger_action_and_close({ option }: { option: Action }) {
    option.action(option.label)
    open = false
  }
</script>

<svelte:window onkeydown={toggle} onclick={close_if_outside} />

{#if open}
  <dialog
    open
    bind:this={dialog}
    transition:fade={{ duration: fade_duration }}
    style={dialog_style}
  >
    <MultiSelect
      options={actions}
      bind:input
      {placeholder}
      onadd={trigger_action_and_close}
      onkeydown={toggle}
      {...rest}
      --sms-bg="var(--sms-options-bg)"
      --sms-width="min(20em, 90vw)"
      --sms-max-width="none"
      --sms-placeholder-color="lightgray"
      --sms-options-margin="1px 0"
      --sms-options-border-radius="0 0 1ex 1ex"
    />
  </dialog>
{/if}

<style>
  :where(dialog) {
    position: fixed;
    top: 30%;
    border: none;
    padding: 0;
    background-color: transparent;
    display: flex;
    color: white;
    z-index: 10;
    font-size: 2.4ex;
  }
</style>