Keyboard-friendly, accessible and highly customizable multi-select component. View the docs
- Bindable:
bind:selected
gives you an array of the currently selected options. Thanks to Svelte's 2-way binding, it can also control the component state externally through assignmentselected = ['foo', 42]
. - Keyboard friendly for mouse-less form completion
- No run-time deps: needs only Svelte as dev dependency
- Dropdowns: scrollable lists for large numbers of options
- Searchable: start typing to filter options
- Tagging: selected options are listed as tags within the input
- Single / multiple select: pass
maxSelect={1, 2, 3, ...}
prop to restrict the number of selectable options - Configurable: see props
Statements | Branches | Lines |
---|---|---|
-
8.0.0 (2022-10-22)Β
- Props
selectedLabels
andselectedValues
were removed. If you were using them, they were equivalent to assigningbind:selected
to a local variable and then runningselectedLabels = selected.map(option => option.label)
andselectedValues = selected.map(option => option.value)
if your options were objects withlabel
andvalue
keys. If they were simple strings/numbers, there was no point in usingselected{Labels,Values}
anyway. PR 138 - Prop
noOptionsMsg
was renamed tonoMatchingOptionsMsg
. PR 133.
- Props
-
v8.3.0 (2023-01-25)Β
addOptionMsg
was renamed tocreateOptionMsg
(no major since version since it's rarely used) sha. -
v9.0.0 (2023-06-01)Β Svelte bumped from v3 to v4. Also, not breaking but noteworthy: MultiSelect received a default slot that functions as both
"option"
and"selected"
. If you previously had two identical slots for"option"
and"selected"
, you can now remove thename
from one of them and drop the other:<MultiSelect {options} + let:option > - <SlotComponent let:option {option} slot="selected" /> - <SlotComponent let:option {option} slot="option" /> + <SlotComponent {option} /> </MultiSelect>
npm install --dev svelte-multiselect
pnpm add -D svelte-multiselect
yarn add --dev svelte-multiselect
<script>
import MultiSelect from 'svelte-multiselect'
const ui_libs = [`Svelte`, `React`, `Vue`, `Angular`, `...`]
let selected = []
</script>
Favorite Frontend Tools?
<code>selected = {JSON.stringify(selected)}</code>
<MultiSelect bind:selected options={ui_libs} />
Full list of props/bindable variables for this component. The Option
type you see below is defined in src/lib/index.ts
and can be imported as import { type Option } from 'svelte-multiselect'
.
-
activeIndex: number | null = null
Zero-based index of currently active option in the array of currently matching options, i.e. if the user typed a search string into the input and only a subset of options match, this index refers to the array position of the matching subset of options
-
activeOption: Option | null = null
Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys.
-
createOptionMsg: string | null = `Create this option...`
The message shown to users when
allowUserOptions
is truthy and they entered text that doesn't match any existing options to suggest creating a new option from the entered text. Emitsconsole.error
ifallowUserOptions
istrue
or'append'
andcreateOptionMsg=''
to since users might be unaware they can create new option. The error can be silenced by settingcreateOptionMsg=null
indicating developer intent is to e.g. use MultiSelect as a tagging component where a user message might be unwanted. -
allowEmpty: boolean = false
Whether to
console.error
if dropdown list of options is empty.allowEmpty={false}
will suppress errors.allowEmpty={true}
will report a console error if component is notdisabled
, not inloading
state and doesn'tallowUserOptions
. -
allowUserOptions: boolean | 'append' = false
Whether users can enter values that are not in the dropdown list.
true
means add user-defined options to the selected list only,'append'
means add to both options and selected. IfallowUserOptions
istrue
or'append'
then the typeobject | number | string
of entered value is determined bytypeof options[0]
(i.e. the first option in the dropdown list) to keep type homogeneity. -
autocomplete: string = `off`
Applied to the
<input>
. Specifies if browser is permitted to auto-fill this form field. Should usually be one of'on'
or'off'
but see MDN docs for other admissible values. -
autoScroll: boolean = true
false
disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. -
breakpoint: number = 800
Screens wider than
breakpoint
in pixels will be considered'desktop'
, everything narrower as'mobile'
. -
defaultDisabledTitle: string = `This option is disabled`
Title text to display when user hovers over a disabled option. Each option can override this through its
disabledTitle
attribute. -
disabled: boolean = false
Disable the component. It will still be rendered but users won't be able to interact with it.
-
disabledInputTitle: string = `This input is disabled`
Tooltip text to display on hover when the component is in
disabled
state.
-
duplicateFunc: (op1: T, op2: T) => boolean = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
This option determines when two options are considered duplicates. Defaults to case-insensitive equality comparison after string coercion (looking only at the
label
key of object options). I.e. the defaultduplicateFunc
considers'Foo' == 'foo'
,'42' == 42
and{ label: `Foo`, value: 0 } == { label: `foo`, value: 42 }
. -
duplicates: boolean = false
Whether to allow users to select duplicate options. Applies only to the selected item list, not the options dropdown. Keeping that free of duplicates is left to developer. The selected item list can have duplicates if
allowUserOptions
is truthy,duplicates
istrue
and users create the same option multiple times. UseduplicateOptionMsg
to customize the message shown to user ifduplicates
isfalse
and users attempt this andduplicateFunc
to customize when a pair of options is considered a duplicate. -
duplicateOptionMsg: string = `This option is already selected`
Text to display to users when
allowUserOptions
is truthy and they try to create a new option that's already selected. -
filterFunc = (op: Option, searchText: string): boolean => { if (!searchText) return true return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase()) }
Customize how dropdown options are filtered when user enters search string into
<MultiSelect />
. Defaults to: -
focusInputOnSelect: boolean | 'desktop' = `desktop`
One of
true
,false
or'desktop'
. Whether to set the cursor back to the input element after selecting an element. 'desktop' means only do so if current window width is larger than the current value ofbreakpoint
prop (default 800). -
form_input: HTMLInputElement | null = null
Handle to the
<input>
DOM node that's responsible for form validity checks and passing selected options to form submission handlers. Only available after component mounts (null
before then). -
highlightMatches: boolean = true
Whether to highlight text in the dropdown options that matches the current user-entered search query. Uses the CSS Custom Highlight API with limited browser support (70% as of May 2023) and styling options. See
::highlight(sms-search-matches)
below for available CSS variables. -
id: string | null = null
Applied to the
<input>
element for associating HTML form<label>
s with this component for accessibility. Also, clicking a<label>
with samefor
attribute asid
will focus this component. -
input: HTMLInputElement | null = null
Handle to the
<input>
DOM node. Only available after component mounts (null
before then). -
inputmode: string | null = null
The
inputmode
attribute hints at the type of data the user may enter. Values like'numeric' | 'tel' | 'email'
allow browsers to display an appropriate virtual keyboard. See MDN for details. -
invalid: boolean = false
If
required = true, 1, 2, ...
and user tries to submit form butselected = []
is empty/selected.length < required
,invalid
is automatically set totrue
and CSS classinvalid
applied to the top-leveldiv.multiselect
.invalid
class is removed as soon as any change toselected
is registered.invalid
can also be controlled externally by binding to it<MultiSelect bind:invalid />
and setting it totrue
based on outside events or custom validation. -
loading: boolean = false
Whether the component should display a spinner to indicate it's in loading state. Use
<slot name='spinner'>
to specify a custom spinner. -
matchingOptions: Option[] = []
List of options currently displayed to the user. Same as
options
unless the user enteredsearchText
in which case this array contains only those options for whichfilterFunc = (op: Option, searchText: string) => boolean
returnedtrue
. -
maxSelect: number | null = null
Positive integer to limit the number of options users can pick.
null
means no limit.maxSelect={1}
will change the type ofselected
to be a singleOption
(ornull
) (not a length-1 array). Likewise, the type ofselectedLabels
changes from(string | number)[]
tostring | number | null
andselectedValues
fromunknown[]
tounknown | null
.maxSelect={1}
will also givediv.multiselect
a class ofsingle
. I.e. you can target the selectordiv.multiselect.single
to give single selects a different appearance from multi selects. -
maxSelectMsg: ((current: number, max: number) => string) | null = ( current: number, max: number ) => (max > 1 ? `${current}/${max}` : ``)
Inform users how many of the maximum allowed options they have already selected. Set
maxSelectMsg={null}
to not show a message. Defaults tonull
whenmaxSelect={1}
ormaxSelect={null}
. Else ifmaxSelect > 1
, defaults to:maxSelectMsg = (current: number, max: number) => `${current}/${max}`
Use CSS selector
span.max-select-msg
(or propmaxSelectMsgClass
if you're using a CSS framework like Tailwind) to customize appearance of the message container. -
minSelect: number | null = null
Conditionally render the
x
button which removes a selected option depending on the number of selected options. Meaning all remove buttons disappear ifselected.length <= minSelect
. E.g. if 2 options are selected andminSelect={3}
, users will not be able to remove any selections until they selected more than 3 options.Note: Prop
required={3}
should be used instead if you only care about the component state at form submission time.minSelect={3}
should be used if you want to place constraints on component state at all times. -
name: string | null = null
Applied to the
<input>
element. Sets the key of this field in a submitted form data object. See form example. -
noMatchingOptionsMsg: string = `No matching options`
What message to show if no options match the user-entered search string.
-
open: boolean = false
Whether the dropdown list is currently visible. Is two-way bindable, i.e. can be used for external control of when the options are visible.
-
options: Option[]
The only required prop (no default). Array of strings/numbers or
Option
objects to be listed in the dropdown. The only required key on objects islabel
which must also be unique. An object'svalue
defaults tolabel
ifundefined
. You can add arbitrary additional keys to your option objects. A few keys likepreselected
andtitle
have special meaning though. See typeObjectOption
insrc/lib/index.ts
for all special keys and their purpose. -
outerDiv: HTMLDivElement | null = null
Handle to outer
<div class="multiselect">
that wraps the whole component. Only available after component mounts (null
before then). -
parseLabelsAsHtml: boolean = false
Whether option labels should be passed to Svelte's
@html
directive or inserted into the DOM as plain text.true
will raise an error ifallowUserOptions
is also truthy as it makes your site susceptible to cross-site scripting (XSS) attacks. -
pattern: string | null = null
The pattern attribute specifies a regular expression which the input's value must match. If a non-null value doesn't match the
pattern
regex, the read-onlypatternMismatch
property will betrue
. See MDN for details. -
placeholder: string | null = null
String shown in the text input when no option is selected.
-
removeAllTitle: string = `Remove all`
Title text to display when user hovers over remove-all button.
-
removeBtnTitle: string = `Remove`
Title text to display when user hovers over button to remove selected option (which defaults to a cross icon).
-
required: boolean | number = false
If
required = true, 1, 2, ...
forms can't be submitted without selecting given number of options.true
means 1.false
means even empty MultiSelect will pass form validity check. If user tries to submit a form containing MultiSelect with less than the required number of options, submission is aborted, MultiSelect scrolls into view and shows message "Please select at leastrequired
options". -
resetFilterOnAdd: boolean = true
Whether text entered into the input to filter options in the dropdown list is reset to empty string when user selects an option.
-
searchText: string = ``
Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text.
-
selected: Option[] = options ?.filter((op) => (op as ObjectOption)?.preselected) .slice(0, maxSelect ?? undefined) ?? []
Array of currently selected options. Supports 2-way binding
bind:selected={[1, 2, 3]}
to control component state externally. Can be passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction. -
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
Default behavior is to render selected items in the order they were chosen.
sortSelected={true}
uses default JS array sorting. A compare function enables custom logic for sorting selected options. See the/sort-selected
example. -
selectedOptionsDraggable: boolean = !sortSelected
Whether selected options are draggable so users can change their order.
-
value: Option | Option[] | null = null
If
maxSelect={1}
,value
will be the single item inselected
(ornull
ifselected
is empty). IfmaxSelect != 1
,maxSelect
andselected
are equal. Warning:value
supports 1-way binding only, meaningbind:value
will updatevalue
when internal component state changes but changingvalue
externally will not update internal component state. This is becausevalue
is already reactive toselected
and makingselected
reactive tovalue
would be cyclic. Suggestions for better solutions that solve both #86 and #136 welcome!
MultiSelect.svelte
accepts the following named slots:
slot="option"
: Customize rendering of dropdown options. Receives as props anoption
and the zero-indexed position (idx
) it has in the dropdown.slot="selected"
: Customize rendering of selected items. Receives as props anoption
and the zero-indexed position (idx
) it has in the list of selected items.slot="spinner"
: Custom spinner component to display when inloading
state. Receives no props.slot="disabled-icon"
: Custom icon to display inside the input when indisabled
state. Receives no props. Use an empty<span slot="disabled-icon" />
ordiv
to remove the default disabled icon.slot="expand-icon"
: Allows setting a custom icon to indicate to users that the Multiselect text input field is expandable into a dropdown list. Receives propopen: boolean
which is true if the Multiselect dropdown is visible and false if it's hidden.slot="remove-icon"
: 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.
Example:
<MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]} let:idx let:option>
<!-- default slot overrides rendering of both dropdown-listed and selected options -->
<span>
{idx + 1}
{option.label}
<span style:background={option.label} style=" width: 1em; height: 1em;" />
</span>
<CustomSpinner slot="spinner">
<strong slot="remove-icon">X</strong>
</MultiSelect>
MultiSelect.svelte
dispatches the following events:
-
on:add={(event) => console.log(event.detail.option)}
Triggers when a new option is selected.
-
on:remove={(event) => console.log(event.detail.option)}`
Triggers when one selected option provided as
event.detail.option
is removed. -
on:removeAll={(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. -
on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option}'`)}
Triggers when an option is either added or removed, or all options are removed at once.
type
is one of'add' | 'remove' | 'removeAll'
and payload will beoption: Option
oroptions: Option[]
, respectively. -
on:open={(event) => console.log(`Multiselect dropdown was opened by ${event}`)}
Triggers when the dropdown list of options appears. Event is the DOM's
FocusEvent
,KeyboardEvent
orClickEvent
that initiated this Sveltedispatch
event. -
on:close={(event) => console.log(`Multiselect dropdown was closed by ${event}`)}
Triggers when the dropdown list of options disappears. Event is the DOM's
FocusEvent
,KeyboardEvent
orClickEvent
that initiated this Sveltedispatch
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
on:change={(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]}
on:keyup={(event) => console.log('key', event.target.value)}
/>
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 slot components. E.g. you'll get an error when trying to use a slot component 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'
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>
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" />
-
div.multiselect
border: var(--sms-border, 1pt solid lightgray)
: Change this to e.g. to1px solid red
to indicate this form field is in an invalid state.border-radius: var(--sms-border-radius, 3pt)
padding: var(--sms-padding, 0 3pt)
background: var(--sms-bg)
color: var(--sms-text-color)
min-height: var(--sms-min-height, 22pt)
width: var(--sms-width)
max-width: var(--sms-max-width)
margin: var(--sms-margin)
font-size: var(--sms-font-size, inherit)
-
div.multiselect.open
z-index: var(--sms-open-z-index, 4)
: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
-
div.multiselect:focus-within
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))
: Border when component has focus. Defaults to--sms-active-color
which in turn defaults tocornflowerblue
.
-
div.multiselect.disabled
background: var(--sms-disabled-bg, lightgray)
: Background when in disabled state.
-
div.multiselect input::placeholder
color: var(--sms-placeholder-color)
opacity: var(--sms-placeholder-opacity)
-
div.multiselect > ul.selected > li
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))
: Background of selected options.padding: var(--sms-selected-li-padding, 1pt 5pt)
: Height of selected options.color: var(--sms-selected-text-color, var(--sms-text-color))
: Text color for selected options.
-
ul.selected > li button:hover, button.remove-all:hover, button:focus
color: var(--sms-remove-btn-hover-color, lightskyblue)
: Color of the remove-icon buttons for removing all or individual selected options when in:focus
or:hover
state.background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2))
: Background for hovered remove buttons.
-
div.multiselect > ul.options
background: var(--sms-options-bg, white)
: Background of dropdown list.max-height: var(--sms-options-max-height, 50vh)
: Maximum height of options dropdown.overscroll-behavior: var(--sms-options-overscroll, none)
: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See MDN.box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black)
: Box shadow of dropdown list.border: var(--sms-options-border)
border-width: var(--sms-options-border-width)
border-radius: var(--sms-options-border-radius, 1ex)
padding: var(--sms-options-padding)
margin: var(--sms-options-margin, inherit)
-
div.multiselect > ul.options > li
scroll-margin: var(--sms-options-scroll-margin, 100px)
: Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.
-
div.multiselect > ul.options > li.selected
background: var(--sms-li-selected-bg)
: Background of selected list items in options pane.color: var(--sms-li-selected-color)
: Text color of selected list items in options pane.
-
div.multiselect > ul.options > li.active
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)))
: Background of active options. Options in the dropdown list become active either by mouseover or by navigating to them with arrow keys. Selected options become active whenselectedOptionsDraggable=true
and an option is being dragged to a new position. Note the active option in that case is not the dragged option but the option under it whose place it will take on drag end.
-
div.multiselect > ul.options > li.disabled
background: var(--sms-li-disabled-bg, #f5f5f6)
: Background of disabled options in the dropdown list.color: var(--sms-li-disabled-text, #b8b8b8)
: Text color of disabled option in the dropdown list.
-
::highlight(sms-search-matches)
: applies to search results in dropdown list that match the current search query ifhighlightMatches=true
. These styles cannot be set via CSS variables. Instead, use a new rule set. For example:::highlight(sms-search-matches) { color: orange; background: rgba(0, 0, 0, 0.15); text-decoration: underline; }
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.
outerDivClass
: wrapperdiv
enclosing the whole componentulSelectedClass
: list of selected optionsliSelectedClass
: selected list itemsulOptionsClass
: available options listed in the dropdown when component is inopen
stateliOptionClass
: list items selectable from dropdown listliActiveOptionClass
: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)maxSelectMsgClass
: small span towards the right end of the input field displaying to the user how many of the allowed number of options they've already selected
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>
</ul>
</div>
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) */
}
Here are some steps to get you started if you'd like to contribute to this project!