Svelte MultiSelect
 Svelte MultiSelect

Tests GitHub Pages NPM version Needs Svelte version REPL Open in StackBlitz

Keyboard-friendly, accessible and highly customizable multi-select component. View the docs

🚀   Getting Started

Simple examples to get you started:

Basic Multi-Select

Pick your favorite fruits:

You selected: []

<script>
  import MultiSelect from '$lib'

  const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']
  let selected = $state([])
</script>

<h3>Pick your favorite fruits:</h3>
<MultiSelect bind:selected options={fruits} placeholder="Choose fruits..." />

<p>You selected: {JSON.stringify(selected)}</p>

Single Select

Pick one color:

You selected: null

<script>
  import MultiSelect from '$lib'

  const colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple']
  let value = $state(null)
</script>

<h3>Pick one color:</h3>
<MultiSelect bind:value options={colors} maxSelect={1} placeholder="Choose a color..." />

<p>You selected: {JSON.stringify(value)}</p>

Object Options

Where have you lived?

Selected countries:

Country codes:

<script>
  import MultiSelect from '$lib'

  const countries = [
    { label: 'United States', value: 'US', continent: 'North America' },
    { label: 'Canada', value: 'CA', continent: 'North America' },
    { label: 'United Kingdom', value: 'UK', continent: 'Europe' },
    { label: 'Germany', value: 'DE', continent: 'Europe' },
    { label: 'Japan', value: 'JP', continent: 'Asia' }
  ]
  let selected = $state([])
</script>

<h3>Where have you lived?</h3>
<MultiSelect bind:selected options={countries} placeholder="Select countries..." />

<p>Selected countries: {selected.map(c => c.label).join(', ')}</p>
<p>Country codes: {selected.map(c => c.value).join(', ')}</p>

With User-Created Options

Add your skills (you can create new ones):

Your skills: []

<script>
  import MultiSelect from '$lib'

  const initial_tags = ['JavaScript', 'Svelte', 'TypeScript']
  let selected = $state([])
</script>

<h3>Add your skills (you can create new ones):</h3>
<MultiSelect
  bind:selected
  options={initial_tags}
  allowUserOptions="append"
  placeholder="Type to add skills..."
/>

<p>Your skills: {JSON.stringify(selected)}</p>

👇   Advanced Examples

selected = []
<script lang="ts">import MultiSelect from '$lib';
import { languages } from '$site/options';
import LanguageSnippet from './LanguageSnippet.svelte';
let selected = $state([]);
</script>

selected = {JSON.stringify(selected) || `[]`}

<MultiSelect
  id="fav-languages"
  options={languages}
  placeholder="Take your pick..."
  bind:selected
>
  {#snippet children({ idx, option })}
    <LanguageSnippet {idx} {option} gap="1ex" />
  {/snippet}
</MultiSelect>
value = null
<script lang="ts">import MultiSelect from '$lib';
import { ml_libs } from '$site/options';
let value = $state(null);
let searchText = $state('');
let loading = $state(false);
$effect(() => {
    loading = Boolean(searchText);
    // perform some fetch/database request here to get list of options matching searchText
    // options = await fetch(`https://example.com?search=${searchText}`)
    setTimeout(async () => { loading = false; }, 1000);
});
</script>

value = {JSON.stringify(value) || `null`}

<MultiSelect
  id="fav-ml-tool"
  maxSelect={1}
  maxSelectMsg={(current, max) => `${current} of ${max} selected`}
  options={ml_libs}
  bind:searchText
  bind:value
  {loading}
  placeholder="Favorite machine learning tool?"
/>
<script lang="ts">import MultiSelect from '$lib';
import { frontend_libs } from '$site/options';
import RepoSnippet from './RepoSnippet.svelte';
import { Confetti } from 'svelte-zoo';
const frontend_libs_filter_func = (op, searchText) => {
    if (!searchText)
        return true;
    const [label, lang, searchStr] = [op.label, op.lang, searchText].map((s) => s.toLowerCase());
    return label.includes(searchStr) || lang.includes(searchStr);
};
let show_confetti = $state(false);
</script>

<MultiSelect
  id="confetti-select"
  options={frontend_libs}
  maxSelect={4}
  placeholder="Favorite web framework?"
  filterFunc={frontend_libs_filter_func}
  on:add={(e) => {
    if (e.detail.option.label === `Svelte`) {
      show_confetti = true
      setTimeout(() => (show_confetti = false), 3000)
    }
  }}
>
 {#snippet option({ idx, option })}
    <RepoSnippet {idx} {option} />
  {/snippet}
</MultiSelect>
{#if show_confetti}
  <Confetti />
{/if}
(due to passing required=true here, form submission will abort if Multiselect is empty)

Also sets allowUserOptions="append" to allow adding custom colors.

<script lang="ts">import MultiSelect from '$lib';
import { colors } from '$site/options';
import ColorSnippet from './ColorSnippet.svelte';
let selected = $state([]);
</script>

<form
  onsubmit={(event) => {
    event.preventDefault()
    alert(`You selected '${selected.join(`, `)}'`)
  }}
>
  <MultiSelect
    id="color-select"
    options={colors}
    bind:selected
    placeholder="Pick some colors..."
    allowUserOptions="append"
    required
  >
    {#snippet children({ idx, option })}
      <ColorSnippet {idx} {option} />
    {/snippet}
  </MultiSelect>
  <button>submit</button>
  (due to passing <code>required={true}</code> here, form submission will abort if
  Multiselect is empty)
  <p>
    Also sets
    <code>allowUserOptions="append"</code> to allow adding custom colors.
  </p>
</form>
<script lang="ts">import MultiSelect from '$lib';
import { countries } from '$site/options';
// required={1} means form validation will prevent submission if no option selected
let maxOptions = $state(10);
</script>

<MultiSelect
  id="countries"
  options={countries}
  required={1}
  minSelect={1}
  maxSelect={1}
  {maxOptions}
  selected={[`Canada`]}
/>

<label>
  maxOptions <input type="range" min=0 max={30} bind:value={maxOptions}>
  {maxOptions} <small>(0 means no limit)</small>
</label>

💡   Features

📝   More examples

Some more in-depth examples for specific features of svelte-multiselect:

🧪   Coverage

StatementsBranchesLines
StatementsBranchesLines

🔨   Installation

npm install --dev svelte-multiselect
pnpm add -D svelte-multiselect
yarn add --dev svelte-multiselect

📙   Usage

<script>
  import MultiSelect from 'svelte-multiselect'

  const ui_libs = [`Svelte`, `React`, `Vue`, `Angular`, `...`]

  let selected = $state([])
</script>

Favorite Frontend Tools?

<code>selected = {JSON.stringify(selected)}</code>

<MultiSelect bind:selected options={ui_libs} />

🧠   Mental Model & Core Concepts

Essential Props

PropPurposeValue
optionsWhat users can choose fromArray of strings, numbers, or objects with label property
bind:selectedWhat they’ve chosenAlways an array: [], ['Apple'] or ['Apple', 'Banana']
bind:valueSingle-select convenience for what users choseSingle item: 'Apple' (or null) if maxSelect={1}, otherwise same as selected

Common Patterns

<!-- Multi-select -->
<MultiSelect bind:selected options={['A', 'B', 'C']} />

<!-- Single-select -->
<MultiSelect bind:value options={colors} maxSelect={1} />

<!-- Object options (need 'label' property) -->
<MultiSelect bind:selected options={[
  { label: 'Red', value: '#ff0000' },
  { label: 'Blue', value: '#0000ff' }
]} />

Troubleshooting

🔣   Props

Complete reference of all props. Props are organized by importance - Essential Props are what you’ll use most often.

💡 Tip: The Option type is automatically inferred from your options array, or you can import it: import { type Option } from 'svelte-multiselect'

Essential Props

These are the core props you’ll use in most cases:

  1. options: Option[]  // REQUIRED

    The only required prop. Array of strings, numbers, or objects that users can select from. Objects must have a label property that will be displayed in the dropdown.

    <!-- Simple options -->
    <MultiSelect options={['Red', 'Green', 'Blue']} />
    
    <!-- Object options -->
    <MultiSelect options={[
      { label: 'Red', value: '#ff0000', hex: true },
      { label: 'Green', value: '#00ff00', hex: true }
    ]} />
  2. selected: Option[] = []  // bindable

    Your main state variable. Array of currently selected options. Use bind:selected for two-way binding.

    <script>
      let selected = $state(['Red'])  // Pre-select Red
    </script>
    <MultiSelect bind:selected options={colors} />
  3. value: Option | Option[] | null = null  // bindable

    Alternative to selected. When maxSelect={1}, value is the single selected item (not an array). Otherwise, value equals selected.

    <!-- Single-select: value = 'Red' (not ['Red']) -->
    <MultiSelect bind:value options={colors} maxSelect={1} />
    
    <!-- Multi-select: value = ['Red', 'Blue'] (same as selected) -->
    <MultiSelect bind:value options={colors} />
  4. maxSelect: number | null = null

    Controls selection behavior. null = unlimited, 1 = single select, 2+ = limited multi-select.

    <!-- Unlimited selection -->
    <MultiSelect options={colors} />
    
    <!-- Single selection -->
    <MultiSelect options={colors} maxSelect={1} />
    
    <!-- Max 3 selections -->
    <MultiSelect options={colors} maxSelect={3} />
  5. placeholder: string | null = null

    Text shown when no options are selected.

  6. disabled: boolean = false

    Disables the component. Users can’t interact with it, but it’s still rendered.

  7. required: boolean | number = false

    For form validation. true means at least 1 option required, numbers specify exact minimum.

Commonly Used Props

  1. searchText: string = `` // bindable

    The text user entered to filter options. Bindable for external control.

  2. open: boolean = false // bindable

    Whether the dropdown is visible. Bindable for external control.

  3. allowUserOptions: boolean | `append` = false

    Whether users can create new options by typing. true = add to selected only, 'append' = add to both options and selected.

  4. allowEmpty: boolean = false

    Whether to allow the component to exist with no options. If false, shows console error when no options provided (unless loading, disabled, or allowUserOptions is true).

  5. loading: boolean = false

    Shows a loading spinner. Useful when fetching options asynchronously.

  6. invalid: boolean = false // bindable

    Marks the component as invalid (adds CSS class). Automatically set during form validation.

Advanced Props

  1. activeIndex: number | null = null  // bindable

    Zero-based index of currently active option in the filtered list.

  2. activeOption: Option | null = null  // bindable

    Currently active option (hovered or navigated to with arrow keys).

  3. createOptionMsg: string | null = `Create this option...`

    Message shown when allowUserOptions is enabled and user can create a new option.

  4. duplicates: boolean = false

    Whether to allow selecting the same option multiple times.

  5. filterFunc: (opt: Option, searchText: string) => boolean

    Custom function to filter options based on search text. Default filters by label.

  6. key: (opt: Option) => unknown

    Function to determine option equality. Default compares by lowercased label.

  7. closeDropdownOnSelect: boolean | 'desktop' = 'desktop'

    Whether to close dropdown after selection. 'desktop' means close on mobile only.

  8. resetFilterOnAdd: boolean = true

    Whether to clear search text when an option is selected.

  9. sortSelected: boolean | ((a: Option, b: Option) => number) = false

    Whether/how to sort selected options. true uses default sort, function enables custom sorting.

  10. portal: { target_node?: HTMLElement; active?: boolean } = {}

    Configuration for portal rendering. When active: true, the dropdown is rendered at document.body level with fixed positioning. Useful for avoiding z-index and overflow issues.

Form & Accessibility Props

  1. id: string | null = null

    Applied to the <input> for associating with <label> elements.

  2. name: string | null = null

    Form field name for form submission.

  3. autocomplete: string = 'off'

    Browser autocomplete behavior. Usually 'on' or 'off'.

  4. inputmode: string | null = null

    Hint for mobile keyboard type ('numeric', 'tel', 'email', etc.). Set to 'none' to hide keyboard.

  5. pattern: string | null = null

    Regex pattern for input validation.

UI & Behavior Props

  1. maxOptions: number | undefined = undefined

    Limit number of options shown in dropdown. undefined = no limit.

  2. minSelect: number | null = null

    Minimum selections required before remove buttons appear.

  3. autoScroll: boolean = true

    Whether to keep active option in view when navigating with arrow keys.

  4. breakpoint: number = 800

    Screen width (px) that separates ‘mobile’ from ‘desktop’ behavior.

  5. highlightMatches: boolean = true

    Whether to highlight matching text in dropdown options.

  6. parseLabelsAsHtml: boolean = false

    Whether to render option labels as HTML. Warning: Don’t combine with allowUserOptions (XSS risk).

  7. selectedOptionsDraggable: boolean = !sortSelected

    Whether selected options can be reordered by dragging.

Message Props

  1. noMatchingOptionsMsg: string = 'No matching options'

    Message when search yields no results.

  2. duplicateOptionMsg: string = 'This option is already selected'

    Message when user tries to create duplicate option.

  3. defaultDisabledTitle: string = 'This option is disabled'

    Tooltip for disabled options.

  4. disabledInputTitle: string = 'This input is disabled'

    Tooltip when component is disabled.

  5. removeAllTitle: string = 'Remove all'

    Tooltip for remove-all button.

  6. removeBtnTitle: string = 'Remove'

    Tooltip for individual remove buttons.

  7. maxSelectMsg: ((current: number, max: number) => string) | null

    Function to generate “X of Y selected” message. null = no message.

DOM Element References (bindable)

These give you access to DOM elements after the component mounts:

  1. input: HTMLInputElement | null = null  // bindable

    Handle to the main <input> DOM element.

  2. form_input: HTMLInputElement | null = null  // bindable

    Handle to the hidden form input used for validation.

  3. outerDiv: HTMLDivElement | null = null  // bindable

    Handle to the outer wrapper <div> element.

Styling Props

For custom styling with CSS frameworks or one-off styles:

  1. style: string | null = null

    CSS rules for the outer wrapper div.

  2. inputStyle: string | null = null

    CSS rules for the main input element.

  3. ulSelectedStyle: string | null = null

    CSS rules for the selected options list.

  4. ulOptionsStyle: string | null = null

    CSS rules for the dropdown options list.

  5. liSelectedStyle: string | null = null

    CSS rules for selected option list items.

  6. liOptionStyle: string | null = null

    CSS rules for dropdown option list items.

CSS Class Props

For use with CSS frameworks like Tailwind:

  1. outerDivClass: string = ''

    CSS class for outer wrapper div.

  2. inputClass: string = ''

    CSS class for main input element.

  3. ulSelectedClass: string = ''

    CSS class for selected options list.

  4. ulOptionsClass: string = ''

    CSS class for dropdown options list.

  5. liSelectedClass: string = ''

    CSS class for selected option items.

  6. liOptionClass: string = ''

    CSS class for dropdown option items.

  7. liActiveOptionClass: string = ''

    CSS class for the currently active dropdown option.

  8. liUserMsgClass: string = ''

    CSS class for user messages (no matches, create option, etc.).

  9. liActiveUserMsgClass: string = ''

    CSS class for active user messages.

  10. maxSelectMsgClass: string = ''

    CSS class for the “X of Y selected” message.

Read-only Props (bindable)

These reflect internal component state:

  1. matchingOptions: Option[] = []  // bindable

    Currently filtered options based on search text.

Bindable Props

selected, value, searchText, open, activeIndex, activeOption, invalid, input, outerDiv, form_input, options, matchingOptions

🎰   Snippets

MultiSelect.svelte accepts the following named snippets:

  1. #snippet option({ option, idx }): Customize rendering of dropdown options. Receives as props an option and the zero-indexed position (idx) it has in the dropdown.
  2. #snippet selectedItem({ option, idx }): Customize rendering of selected items. Receives as props an option and the zero-indexed position (idx) it has in the list of selected items.
  3. #snippet spinner(): Custom spinner component to display when in loading state. Receives no props.
  4. #snippet disabledIcon(): Custom icon to display inside the input when in disabled state. Receives no props. Use an empty {#snippet disabledIcon()}{/snippet} to remove the default disabled icon.
  5. #snippet expandIcon(): Allows setting a custom icon to indicate to users that the Multiselect text input field is expandable into a dropdown list. Receives prop open: boolean which is true if the Multiselect dropdown is visible and false if it’s hidden.
  6. #snippet removeIcon(): Custom icon to display as remove button. Will be used both by buttons to remove individual selected options and the ‘remove all’ button that clears all options at once. Receives no props.
  7. #snippet userMsg({ searchText, msgType, msg }): Displayed like a dropdown item when the list is empty and user is allowed to create custom options based on text input (or if the user’s text input clashes with an existing option). Receives props:
    • searchText: The text user typed into search input.
    • msgType: false | 'create' | 'dupe' | 'no-match': 'dupe' means user input is a duplicate of an existing option. 'create' means user is allowed to convert their input into a new option not previously in the dropdown. 'no-match' means user input doesn’t match any dropdown items and users are not allowed to create new options. false means none of the above.
    • msg: Will be duplicateOptionMsg or createOptionMsg (see props) based on whether user input is a duplicate or can be created as new option. Note this snippet replaces the default UI for displaying these messages so the snippet needs to render them instead (unless purposely not showing a message).
  8. snippet='after-input': Placed after the search input. For arbitrary content like icons or temporary messages. Receives props selected: Option[], disabled: boolean, invalid: boolean, id: string | null, placeholder: string, open: boolean, required: boolean. Can serve as a more dynamic, more customizable alternative to the placeholder prop.

Example using several snippets:

<MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]}>
  {#snippet children({ idx, option })}
    <span style="display: flex; align-items: center; gap: 6pt;">
      <span
        style:background={`${option}`}
        style="border-radius: 50%; width: 1em; height: 1em;"
      ></span>
      {idx + 1}
      {option}
    </span>
  {/snippet}
  {#snippet spinner()}
    <CustomSpinner />
  {/snippet}
  {#snippet removeIcon()}
    <strong>X</strong>
  {/snippet}
</MultiSelect>

🎬   Events

MultiSelect.svelte dispatches the following events:

  1. onadd={(event) => console.log(event.detail.option)}

    Triggers when a new option is selected. The newly selected option is provided as event.detail.option.

  2. oncreate={(event) => console.log(event.detail.option)}

    Triggers when a user creates a new option (when allowUserOptions is enabled). The created option is provided as event.detail.option.

  3. onremove={(event) => console.log(event.detail.option)}`

    Triggers when a single selected option is removed. The removed option is provided as event.detail.option.

  4. onremoveAll={(event) => console.log(event.detail.options)}`

    Triggers when all selected options are removed. The payload event.detail.options gives the options that were previously selected.

  5. onchange={(event) => console.log(`${event.detail.type}: '${event.detail.option}'`)}

    Triggers when an option is either added (selected) or removed from selected, or all selected options are removed at once. type is one of 'add' | 'remove' | 'removeAll' and payload will be option: Option or options: Option[], respectively.

  6. onopen={(event) => console.log(`Multiselect dropdown was opened by ${event}`)}

    Triggers when the dropdown list of options appears. Event is the DOM’s FocusEvent,KeyboardEvent or ClickEvent that initiated this Svelte dispatch event.

  7. onclose={(event) => console.log(`Multiselect dropdown was closed by ${event}`)}

    Triggers when the dropdown list of options disappears. Event is the DOM’s FocusEvent, KeyboardEvent or ClickEvent that initiated this Svelte dispatch event.

For example, here’s how you might annoy your users with an alert every time one or more options are added or removed:

<MultiSelect
  onchange={(e) => {
    if (e.detail.type === 'add') alert(`You added ${e.detail.option}`)
    if (e.detail.type === 'remove') alert(`You removed ${e.detail.option}`)
    if (e.detail.type === 'removeAll') alert(`You removed ${e.detail.options}`)
  }}
/>

Note: Depending on the data passed to the component the options(s) payload will either be objects or simple strings/numbers.

The above list of events are Svelte dispatch events. This component also forwards many DOM events from the <input> node: blur, change, click, keydown, keyup, mousedown, mouseenter, mouseleave, touchcancel, touchend, touchmove, touchstart. Registering listeners for these events works the same:

<MultiSelect
  options={[1, 2, 3]}
  onkeyup={(event) => console.log('key', event.target.value)}
/>

🦺   TypeScript

The type of options is inferred automatically from the data you pass. E.g.

const options = [
   { label: `foo`, value: 42 }
   { label: `bar`, value: 69 }
]
// type Option = { label: string, value: number }
const options = [`foo`, `bar`]
// type Option = string
const options = [42, 69]
// type Option = number

The inferred type of Option is used to enforce type-safety on derived props like selected as well as snippets. E.g. you’ll get an error when trying to use a snippet that expects a string if your options are objects (see this comment for example screenshots).

You can also import the types this component uses for downstream applications:

import {
  Option,
  ObjectOption,
  DispatchEvents,
  MultiSelectEvents,
} from 'svelte-multiselect'

✨   Styling

There are 3 ways to style this component. To understand which options do what, it helps to keep in mind this simplified DOM structure of the component:

<div class="multiselect">
  <ul class="selected">
    <li>Selected 1</li>
    <li>Selected 2</li>
  </ul>
  <ul class="options">
    <li>Option 1</li>
    <li>Option 2</li>
  </ul>
</div>

With CSS variables

If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a :global() CSS context. See app.css for how these variables are set on the demo site of this component.

Minimal example that changes the background color of the options dropdown:

<MultiSelect --sms-options-bg="white" />

With CSS frameworks

The second method allows you to pass in custom classes to the important DOM elements of this component to target them with frameworks like Tailwind CSS.

This simplified version of the DOM structure of the component shows where these classes are inserted:

<div class="multiselect {outerDivClass}">
  <input class={inputClass} />
  <ul class="selected {ulSelectedClass}">
    <li class={liSelectedClass}>Selected 1</li>
    <li class={liSelectedClass}>Selected 2</li>
  </ul>
  <span class="maxSelectMsgClass">2/5 selected</span>
  <ul class="options {ulOptionsClass}">
    <li class={liOptionClass}>Option 1</li>
    <li class="{liOptionClass} {liActiveOptionClass}">
      Option 2 (currently active)
    </li>
    ...
    <li class="{liUserMsgClass} {liActiveUserMsgClass}">
      Create this option...
    </li>
  </ul>
</div>

With global CSS

Odd as it may seem, you get the most fine-grained control over the styling of every part of this component by using the following :global() CSS selectors. ul.selected is the list of currently selected options rendered inside the component’s input whereas ul.options is the list of available options that slides out when the component is in its open state. See also simplified DOM structure.

:global(div.multiselect) {
  /* top-level wrapper div */
}
:global(div.multiselect.open) {
  /* top-level wrapper div when dropdown open */
}
:global(div.multiselect.disabled) {
  /* top-level wrapper div when in disabled state */
}
:global(div.multiselect > ul.selected) {
  /* selected list */
}
:global(div.multiselect > ul.selected > li) {
  /* selected list items */
}
:global(div.multiselect button) {
  /* target all buttons in this component */
}
:global(div.multiselect > ul.selected > li button, button.remove-all) {
  /* buttons to remove a single or all selected options at once */
}
:global(div.multiselect > input[autocomplete]) {
  /* input inside the top-level wrapper div */
}
:global(div.multiselect > ul.options) {
  /* dropdown options */
}
:global(div.multiselect > ul.options > li) {
  /* dropdown list items */
}
:global(div.multiselect > ul.options > li.selected) {
  /* selected options in the dropdown list */
}
:global(div.multiselect > ul.options > li:not(.selected):hover) {
  /* unselected but hovered options in the dropdown list */
}
:global(div.multiselect > ul.options > li.active) {
  /* active means item was navigated to with up/down arrow keys */
  /* ready to be selected by pressing enter */
}
:global(div.multiselect > ul.options > li.disabled) {
  /* options with disabled key set to true (see props above) */
}

🆕   Changelog

View the changelog.

🙏   Contributing

Here are some steps to get you started if you’d like to contribute to this project!