« 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'
  import { routes } from '../index'

  const actions = routes.map(({ route }) => ({
    label: route,
    action: () => goto(route),
  }))
</script>

<CmdPalette {actions} />
  1. CmdPalette.svelte source code
    <script lang="ts">
      import { MultiSelect } from '$lib'
      import { fade } from 'svelte/transition'
      import type { MultiSelectProps, ObjectOption, Option } 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 && document.activeElement !== input) {
          input.focus()
        }
      })
    
      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(data: { option: Option }) {
        const { action, label } = data.option as Action
        action(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>