« 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 { tick } from 'svelte'
  import { fade } from 'svelte/transition'
  import Select from './MultiSelect.svelte'

  export let actions: Action[]
  export let triggers: string[] = [`k`]
  export let close_keys: string[] = [`Escape`]
  export let fade_duration: number = 200 // in ms
  export let style: string = `` // for dialog
  // for span in option slot, has no effect when passing a slot
  export let span_style: string = ``
  export let open: boolean = false
  export let dialog: HTMLDialogElement | null = null
  export let input: HTMLInputElement | null = null
  export let placeholder: string = `Filter actions...`

  type Action = { label: string; action: () => void }

  async function toggle(event: KeyboardEvent) {
    if (triggers.includes(event.key) && event.metaKey && !open) {
      // open on cmd+trigger
      open = true
      await tick() // wait for dialog to open and input to be mounted
      input?.focus()
    } 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(event: CustomEvent<{ option: Action }>) {
    event.detail.option.action(event.detail.option.label)
    open = false
  }
</script>

<svelte:window on:keydown={toggle} on:click={close_if_outside} />

{#if open}
  <dialog open bind:this={dialog} transition:fade={{ duration: fade_duration }} {style}>
    <Select
      options={actions}
      bind:input
      {placeholder}
      on:add={trigger_action_and_close}
      on:keydown={toggle}
      {...$$restProps}
      let:option
    >
      <!-- wait for https://github.com/sveltejs/svelte/pull/8304 -->
      <slot>
        <span style={span_style}>{option.label}</span>
      </slot>
    </Select>
  </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;
  }
  dialog :global(div.multiselect) {
    --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;
  }
</style>