Keyboard-friendly, accessible and highly customizable multi-select component.
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 assignment selected = ['foo', 42].maxSelect={1, 2, 3, ...} prop to restrict the number of selectable options| Statements | Branches | Lines |
|---|---|---|
npm install --dev svelte-multiselect
<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} />
| Prop | Purpose | Value |
|---|---|---|
options |
What users can choose from | Array of strings, numbers, or objects with label property |
bind:selected |
Which options users have chosen | Always an array: [], ['Apple'] or ['Apple', 'Banana'] |
bind:value |
Single-select convenience for the user-selected option | Single item: 'Apple' (or null) if maxSelect={1}, otherwise same as selected |
<!-- Multi-select -->
<MultiSelect bind:selected options={['A', 'B', 'C']} />
<!-- Single-select -->
<MultiSelect bind:value options={colors} maxSelect={1} />
<!-- Object options (need 'label' property, can have arbitrary other keys, some like `value`, `disabled`, `preselected`, `style` have special meaning, see type ObjectOption) -->
<MultiSelect
bind:selected
options={[
{ label: 'Red', value: '#ff0000' },
{ label: 'Blue', value: '#0000ff' },
]}
/>
label propertyoptions and not disabled={true}bind:value with maxSelect={1}selected and value from your options arrayComplete reference of all props. Props are organized by importance - Essential Props are what you'll use most often.
๐ก Tip: The
Optiontype is automatically inferred from youroptionsarray, or you can import it:import { type Option } from 'svelte-multiselect'
These are the core props you'll use in most cases:
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 },
]}
/>
selected: Option[] = [] // bindable
Your main state variable. Array of currently selected options. Use bind:selected for two-way binding.
<script>
let selected = $state(['Red']) // Preselect Red
</script>
<MultiSelect bind:selected options={colors} />
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} />
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} />
placeholder: string | { text: string; persistent?: boolean } | null = null
Text shown when no options are selected. Can be a simple string or an object with extended options:
<!-- Simple string -->
<MultiSelect placeholder="Choose..." />
<!-- Object with persistent option (stays visible even when options selected) -->
<MultiSelect placeholder={{ text: 'Add items...', persistent: true }} />
disabled: boolean = false
Disables the component. Users can't interact with it, but it's still rendered.
required: boolean | number = false
For form validation. true means at least 1 option required, numbers specify exact minimum.
searchText: string = `` // bindable
The text user entered to filter options. Bindable for external control.
open: boolean = false // bindable
Whether the dropdown is visible. Bindable for external control.
allowUserOptions: boolean | `append` = false
Whether users can create new options by typing. true = add to selected only, 'append' = add to both options and selected.
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).
loading: boolean = false
Shows a loading spinner. Useful when fetching options asynchronously.
invalid: boolean = false // bindable
Marks the component as invalid (adds CSS class). Automatically set during form validation.
loadOptions: LoadOptionsFn | LoadOptionsConfig = undefined
Dynamic loading for large datasets. Enables lazy loading / infinite scroll instead of passing static options. Pass either a function or an object with config:
<!-- Simple: just a function -->
<MultiSelect loadOptions={myFetchFn} />
<!-- With config -->
<MultiSelect loadOptions={{ fetch: myFetchFn, debounceMs: 500, batchSize: 20 }} />
The function receives { search, offset, limit } and must return { options, hasMore }:
async function load_options({ search, offset, limit }) {
const response = await fetch(`/api/items?q=${search}&skip=${offset}&take=${limit}`)
const { items, total } = await response.json()
return { options: items, hasMore: offset + limit < total }
}
Config options (when passing an object):
| Key | Type | Default | Description |
|---|---|---|---|
fetch |
fn |
โ | Async function to load options (required) |
debounceMs |
number |
300 |
Debounce delay for search queries |
batchSize |
number |
50 |
Number of options to load per batch |
onOpen |
boolean |
true |
Whether to load options when dropdown opens |
Features automatic state management, debounced search, infinite scroll pagination, and loading indicators. See the infinite-scroll demo for live examples.
activeIndex: number | null = null // bindable
Zero-based index of currently active option in the filtered list.
activeOption: Option | null = null // bindable
Currently active option (hovered or navigated to with arrow keys).
createOptionMsg: string | ((state: { searchText: string; selected: Option[]; options: Option[]; matchingOptions: Option[] }) => string) | null = `Create this option...`
Message shown when allowUserOptions is enabled and user can create a new option. Can be a static string or a function that receives component state and returns a dynamic message.
duplicates: boolean | 'case-insensitive' = false
Controls duplicate detection. false (default) blocks exact duplicates. true allows selecting the same option multiple times. 'case-insensitive' blocks case variants (e.g. "Apple" blocks "apple").
filterFunc: (opt: Option, searchText: string) => boolean
Custom function to filter options based on search text. Default filters by label.
key: (opt: Option) => unknown
Function to determine option equality. Default compares by lowercased label.
closeDropdownOnSelect: boolean | 'if-mobile' | 'retain-focus' = false
Whether to close dropdown after selection. false (default) keeps dropdown open for rapid multi-selection. true closes after each selection. 'if-mobile' closes on mobile devices only (screen width below breakpoint). 'retain-focus' closes dropdown but keeps input focused for rapid typing to create custom options from text input (see allowUserOptions).
resetFilterOnAdd: boolean = true
Whether to clear search text when an option is selected.
sortSelected: boolean | ((a: Option, b: Option) => number) = false
Whether/how to sort selected options. true uses default sort, function enables custom sorting.
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.
Group related options together with visual headers. Add a group key to your option objects:
<script>
const options = [
{ label: `JavaScript`, group: `Frontend` },
{ label: `TypeScript`, group: `Frontend` },
{ label: `Python`, group: `Backend` },
{ label: `Go`, group: `Backend` },
]
</script>
<MultiSelect {options} collapsibleGroups groupSelectAll />
See the grouping demo for live examples.
collapsibleGroups: boolean = false
Enable click-to-collapse groups. When true, users can click group headers to hide/show options in that group.
collapsedGroups: Set<string> = new Set()
Bindable set of collapsed group names. Use bind:collapsedGroups to control which groups are collapsed externally or to persist collapse state.
groupSelectAll: boolean = false
Add a "Select all" button to each group header, allowing users to select all options in a specific group at once.
ungroupedPosition: 'first' | 'last' = 'first'
Where to render options that don't have a group key. 'first' places them at the top, 'last' at the bottom.
groupSortOrder: 'none' | 'asc' | 'desc' | ((a: string, b: string) => number) = 'none'
Sort groups alphabetically ('asc' or 'desc') or with a custom comparator function. Default 'none' preserves order of first occurrence.
searchExpandsCollapsedGroups: boolean = false
When true, collapsed groups automatically expand when the search query matches options within them.
searchMatchesGroups: boolean = false
When true, the search query also matches against group names, not just option labels. If a group name matches, all options in that group are shown.
keyboardExpandsCollapsedGroups: boolean = false
When true, collapsed groups automatically expand when the user navigates into them with arrow keys.
stickyGroupHeaders: boolean = false
When true, group headers stick to the top of the dropdown while scrolling through their options.
collapseAllGroups: () => void // bindable
Programmatically collapse all groups. Use with bind:collapseAllGroups to get a callable function.
expandAllGroups: () => void // bindable
Programmatically expand all groups. Use with bind:expandAllGroups to get a callable function.
liGroupHeaderClass: string = ''
CSS class applied to group header <li> elements.
liGroupHeaderStyle: string | null = null
Inline style for group header elements.
groupHeader: Snippet<[{ group: string; options: T[]; collapsed: boolean }]>
Custom snippet for rendering group headers. Receives the group name, array of options in that group, and whether the group is collapsed.
ongroupToggle: (data: { group: string; collapsed: boolean }) => void
Callback fired when a group is collapsed or expanded. Receives the group name and its new collapsed state.
id: string | null = null
Applied to the <input> for associating with <label> elements.
name: string | null = null
Form field name for form submission.
autocomplete: string = 'off'
Browser autocomplete behavior. Usually 'on' or 'off'.
inputmode: string | null = null
Hint for mobile keyboard type ('numeric', 'tel', 'email', etc.). Set to 'none' to hide keyboard.
pattern: string | null = null
Regex pattern for input validation.
maxOptions: number | undefined = undefined
Limit number of options shown in dropdown. undefined = no limit.
minSelect: number | null = null
Minimum selections required before remove buttons appear.
autoScroll: boolean = true
Whether to keep active option in view when navigating with arrow keys.
breakpoint: number = 800
Screen width (px) that separates 'mobile' from 'desktop' behavior.
fuzzy: boolean = true
Whether to use fuzzy matching for filtering options. When true (default), matches non-consecutive characters (e.g., "ga" matches "Grapes" and "Green Apple"). When false, uses substring matching only.
highlightMatches: boolean = true
Whether to highlight matching text in dropdown options.
keepSelectedInDropdown: false | 'plain' | 'checkboxes' = false
Controls whether selected options remain visible in dropdown. false (default) hides selected options. 'plain' shows them with visual distinction. 'checkboxes' prefixes each option with a checkbox.
selectAllOption: boolean | string = false
Adds a "Select All" option at the top of the dropdown. true shows default label, or pass a custom string label.
liSelectAllClass: string = ''
CSS class applied to the "Select All" <li> element.
parseLabelsAsHtml: boolean = false
Whether to render option labels as HTML. Warning: Don't combine with allowUserOptions (XSS risk).
selectedOptionsDraggable: boolean = !sortSelected
Whether selected options can be reordered by dragging.
selectedFlipParams: FlipParams = { duration: 100 }
Animation parameters for the Svelte flip animation when reordering selected options via drag-and-drop. Set { duration: 0 } to disable animation. Accepts duration, delay, and easing properties.
shortcuts: Partial<KeyboardShortcuts> = {}
Override default keyboard shortcuts. Shortcut format: "modifier+...+key" where modifiers can be ctrl, shift, alt, meta, cmd. Set a shortcut to null to disable it. Custom shortcuts take precedence over built-in key handlers (Enter, Escape, ArrowUp/Down, Backspace).
Available shortcuts and their defaults:
| Key | Default | Action |
|---|---|---|
select_all |
'ctrl+a' |
Select all visible options |
clear_all |
'ctrl+shift+a' |
Deselect all options |
open |
null |
Open dropdown |
close |
null |
Close dropdown (Escape works by default) |
undo |
'meta+z' / 'ctrl+z' |
Undo last selection change (platform-aware) |
redo |
'meta+shift+z' / 'ctrl+shift+z' |
Redo last undone change (platform-aware) |
history: boolean | number = true
Enable selection history for undo/redo support. true (default) stores up to 50 states. Pass a number to set a custom maximum. false or 0 disables history. Note: you need at least history=2 for a single undo (history=1 effectively disables it).
undo: () => boolean // bindable
Undo the last selection change. Returns true if undo was performed. Use with bind:undo to get a callable function.
redo: () => boolean // bindable
Redo the last undone selection change. Returns true if redo was performed. Use with bind:redo to get a callable function.
canUndo: boolean // bindable
Whether an undo operation is available. Use with bind:canUndo for UI indicators (e.g. disabling an undo button).
canRedo: boolean // bindable
Whether a redo operation is available. Use with bind:canRedo for UI indicators (e.g. disabling a redo button).
noMatchingOptionsMsg: string = 'No matching options'
Message when search yields no results.
duplicateOptionMsg: string = 'This option is already selected'
Message when user tries to create duplicate option.
defaultDisabledTitle: string = 'This option is disabled'
Tooltip for disabled options.
disabledInputTitle: string = 'This input is disabled'
Tooltip when component is disabled.
removeAllTitle: string = 'Remove all'
Tooltip for remove-all button.
removeBtnTitle: string = 'Remove'
Tooltip for individual remove buttons.
maxSelectMsg: ((current: number, max: number) => string) | null
Function to generate "X of Y selected" message. null = no message.
These give you access to DOM elements after the component mounts:
input: HTMLInputElement | null = null // bindable
Handle to the main <input> DOM element.
form_input: HTMLInputElement | null = null // bindable
Handle to the hidden form input used for validation.
outerDiv: HTMLDivElement | null = null // bindable
Handle to the outer wrapper <div> element.
For custom styling with CSS frameworks or one-off styles:
style: string | null = null
CSS rules for the outer wrapper div.
inputStyle: string | null = null
CSS rules for the main input element.
ulSelectedStyle: string | null = null
CSS rules for the selected options list.
ulOptionsStyle: string | null = null
CSS rules for the dropdown options list.
liSelectedStyle: string | null = null
CSS rules for selected option list items.
liOptionStyle: string | null = null
CSS rules for dropdown option list items.
For use with CSS frameworks like Tailwind:
outerDivClass: string = ''
CSS class for outer wrapper div.
inputClass: string = ''
CSS class for main input element.
ulSelectedClass: string = ''
CSS class for selected options list.
ulOptionsClass: string = ''
CSS class for dropdown options list.
liSelectedClass: string = ''
CSS class for selected option items.
liOptionClass: string = ''
CSS class for dropdown option items.
liActiveOptionClass: string = ''
CSS class for the currently active dropdown option.
liUserMsgClass: string = ''
CSS class for user messages (no matches, create option, etc.).
liActiveUserMsgClass: string = ''
CSS class for active user messages.
maxSelectMsgClass: string = ''
CSS class for the "X of Y selected" message.
These reflect internal component state:
matchingOptions: Option[] = [] // bindable
Currently filtered options based on search text.
selected, value, searchText, open, activeIndex, activeOption, invalid, input, outerDiv, form_input, options, matchingOptions, collapsedGroups, collapseAllGroups, expandAllGroups, undo, redo, canUndo, canRedo
MultiSelect.svelte accepts the following named snippets:
#snippet option({ option, idx, selected, active, disabled }): Customize rendering of dropdown options. Receives the option, its zero-indexed position (idx) in the dropdown, whether it is selected, active (keyboard-highlighted), and disabled.#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.#snippet children({ option, idx, type }): Convenience snippet that applies to both dropdown options AND selected items. Use this when you want the same custom rendering for both. Takes precedence if option or selectedItem are not provided. type is 'selected' when rendering a selected pill and 'option' when rendering a dropdown item, allowing conditional styling/content by context.#snippet spinner(): Custom spinner component to display when in loading state. Receives no props.#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.#snippet expandIcon({ open, disabled }): Allows setting a custom icon to indicate to users that the Multiselect text input field is expandable into a dropdown list. open is true if the dropdown is visible and false if hidden. disabled reflects the component's disabled state.#snippet removeIcon({ option, isRemoveAll }): Custom icon to display as remove button. Used both by per-option remove buttons (isRemoveAll: false, option is the item being removed) and the 'remove all' button (isRemoveAll: true, option is undefined).#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).#snippet afterInput({ selected, disabled, invalid, id, placeholder, open, required, searchText }): Placed after the search input. For arbitrary content like icons or temporary messages. 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, type })}
<span style="display: flex; align-items: center; gap: 6pt">
<span
style:background={`${option}`}
style="border-radius: 50%; width: 1em; height: 1em"
></span>
{#if type === `option`}{idx + 1}{/if}
{option}
</span>
{/snippet}
{#snippet spinner()}
<CustomSpinner />
{/snippet}
{#snippet removeIcon({ isRemoveAll })}
<strong>{isRemoveAll ? `Clear` : `X`}</strong>
{/snippet}
</MultiSelect>
MultiSelect.svelte provides the following event callback props:
onadd={({ option, selected }) => console.log(option, selected)}
Triggers when a new option is selected. option is the newly selected option, selected is the updated array of all selected options.
oncreate={({ option }) => console.log(option)}
Triggers when a user creates a new option (when allowUserOptions is enabled). The created option is provided as option.
onremove={({ option, selected }) => console.log(option, selected)}
Triggers when a single selected option is removed. option is the removed option, selected is the updated array of remaining selected options.
onremoveAll={({ options }) => console.log(options)}
Triggers when all selected options are removed. The options payload gives the options that were removed (might not be all if minSelect is set).
onselectAll={({ options }) => console.log(options)}
Triggers when the "Select All" option is clicked (requires selectAllOption to be enabled). The options payload contains the options that were added.
onreorder={({ options, previous }) => console.log(options, previous)}
Triggers when selected options are reordered via drag-and-drop (enabled by default when sortSelected is false). options is the newly ordered array, previous is the array before reordering.
onchange={({ type, option, options }) => console.log(type, option ?? options)}
Triggers when an option is either added (selected) or removed from selected, all selected options are removed at once, or selected options are reordered via drag-and-drop. type is one of 'add' | 'remove' | 'removeAll' | 'selectAll' | 'reorder' and payload will be option: Option or options: Option[], respectively.
onopen={({ event }) => console.log(`Dropdown opened by`, event)}
Triggers when the dropdown list of options appears. event is the DOM's FocusEvent, KeyboardEvent or ClickEvent that triggered the open.
onclose={({ event }) => console.log(`Dropdown closed by`, event)}
Triggers when the dropdown list of options disappears. event is the DOM's FocusEvent, KeyboardEvent or ClickEvent that triggered the close.
onsearch={({ searchText, matchingOptions }) => console.log(searchText, matchingOptions.length)}
Triggers (debounced, 150ms) when the search text changes. Useful for analytics or loading remote options. searchText is the current input value, matchingOptions is the array of options matching the search.
onmaxreached={({ selected, maxSelect, attemptedOption }) => console.log(attemptedOption)}
Triggers when a user tries to select more options than maxSelect allows. Useful for showing feedback. Does not fire for maxSelect=1 (which uses replace behavior).
onduplicate={({ option }) => console.log(`Duplicate:`, option)}
Triggers when a user tries to add an already-selected option (when duplicates=false). Useful for showing feedback to the user.
onactivate={({ option, index }) => console.log(`Active:`, option, index)}
Triggers during keyboard navigation (ArrowUp/ArrowDown) through options. option is the newly active option, index is its position. Does not fire on mouse hover.
oncollapseAll={({ groups }) => console.log(`Collapsed:`, groups)}
Triggers when all groups are collapsed (e.g. via collapseAllGroups()). groups lists the group names that were collapsed.
onexpandAll={({ groups }) => console.log(`Expanded:`, groups)}
Triggers when all groups are expanded (e.g. via expandAllGroups()). groups lists the group names that were expanded.
onundo={({ previous, current }) => console.log(`Undo:`, previous, `โ`, current)}
Triggers when an undo operation restores a previous selection state. previous is the selection before undo, current is the restored selection.
onredo={({ previous, current }) => console.log(`Redo:`, previous, `โ`, current)}
Triggers when a redo operation re-applies a previously undone selection change. previous is the selection before redo, current is the new selection.
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={({ type, option, options }) => {
if (type === 'add') alert(`You added ${option}`)
if (type === 'remove') alert(`You removed ${option}`)
if (type === 'removeAll') alert(`You removed ${options}`)
if (type === 'selectAll') alert(`You selected all: ${options}`)
if (type === 'reorder') alert(`New order: ${options}`)
}}
/>
Note: Depending on the data passed to the component the
option(s)payload will either be objects or simple strings/numbers.
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)}
/>
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 {
LoadOptions, // Dynamic option loading callback
LoadOptionsConfig,
LoadOptionsFn,
LoadOptionsParams,
LoadOptionsResult,
MultiSelectEvents,
MultiSelectSnippets,
ObjectOption,
Option,
} from 'svelte-multiselect'
This package also provides subpath exports for utilities used by the component:
import {
click_outside,
draggable,
highlight_matches,
sortable,
tooltip,
} from 'svelte-multiselect/attachments'
import { fuzzy_match, get_label } from 'svelte-multiselect/utils'
import { heading_anchors } from 'svelte-multiselect/heading-anchors'
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. All variables have sensible defaults defined inside MultiSelect.svelte itself.
Minimal example that changes the background color of the options dropdown:
<MultiSelect --sms-options-bg="white" />
div.multiselect
border: var(--sms-border, 1pt solid light-dark(lightgray, #555)): Change this to e.g. to 1px 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, light-dark(white, #222226))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 to cornflowerblue.div.multiselect.disabled
background: var(--sms-disabled-bg, light-dark(lightgray, #444)): 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, light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15))): Background of selected options.padding: var(--sms-selected-li-padding, 0 2pt 0 5pt): Padding 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, inherit): 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, light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2))): Background for hovered remove buttons.div.multiselect > ul.options
background: var(--sms-options-bg, light-dark(#fafafa, #222226)): 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.z-index: var(--sms-options-z-index, 3): Z-index for the dropdown options list.box-shadow: var(--sms-options-shadow, light-dark(0 0 14pt -8pt black, 0 0 14pt -4pt rgba(0, 0, 0, 0.8))): 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, 6pt 0 0 0)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-plain-bg, light-dark(rgba(0, 123, 255, 0.1), rgba(100, 180, 255, 0.2))): Background of selected list items in options pane.border-left: var(--sms-li-selected-plain-border, 3px solid var(--sms-active-color, cornflowerblue)): Left border of selected list items in options pane.div.multiselect > ul.options > li.active
background: var(--sms-li-active-bg, var(--sms-active-color, light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 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 when selectedOptionsDraggable=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, light-dark(#f5f5f6, #2a2a2a)): Background of disabled options in the dropdown list.color: var(--sms-li-disabled-text, light-dark(#b8b8b8, #666)): Text color of disabled option in the dropdown list.div.multiselect > ul.options > li.select-all
border-bottom: var(--sms-select-all-border-bottom, 1px solid light-dark(lightgray, #555)): Bottom border separating "Select All" from regular options.font-weight: var(--sms-select-all-font-weight, 500): Font weight of "Select All" text.color: var(--sms-select-all-color, inherit): Text color of "Select All" option.background: var(--sms-select-all-bg, transparent): Background of "Select All" option.margin-bottom: var(--sms-select-all-margin-bottom, 2pt): Space below "Select All" option.background (hover): var(--sms-select-all-hover-bg, ...): Background of "Select All" on hover. Falls back to --sms-li-active-bg then --sms-active-color.div.multiselect > ul.options > li.group-header
font-weight: var(--sms-group-header-font-weight, 600): Font weight of group headers.font-size: var(--sms-group-header-font-size, 0.9em): Font size of group headers.color: var(--sms-group-header-color, light-dark(#666, #aaa)): Text color of group headers.background: var(--sms-group-header-bg, transparent): Background of group headers.padding: var(--sms-group-header-padding, 2pt 1ex): Padding around group header text.text-transform: var(--sms-group-header-text-transform, uppercase): Text transform for group headers.letter-spacing: var(--sms-group-header-letter-spacing, 0.5px): Letter spacing for group headers.margin-top: var(--sms-group-header-margin-top, 4pt): Top margin for group headers (except the first).border-top: var(--sms-group-header-border-top, 1px solid light-dark(#eee, #333)): Top border for group headers (except the first).background (hover): var(--sms-group-header-hover-bg, light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05))): Background of collapsible group headers on hover.background (sticky): var(--sms-group-header-sticky-bg, ...): Background when stickyGroupHeaders is enabled. Falls back to --sms-options-bg.div.multiselect > ul.options > li (grouped options)
padding-left: var(--sms-group-item-padding-left, var(--sms-group-option-indent, 1.5ex)): Indentation for options within a group.Group chevron icon
transition: transform var(--sms-group-collapse-duration, 0.15s) ease-out: Animation duration for group collapse/expand chevron rotation.Group "Select/Deselect All" button
background (hover): var(--sms-group-select-all-hover-bg, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1))): Background of per-group select-all button on hover.color (deselect): var(--sms-group-deselect-color, light-dark(#c44, #f77)): Text color of "Deselect All" button (when all group options are already selected).::highlight(sms-search-matches): applies to search results in dropdown list that match the current search query if highlightMatches=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: wrapper div enclosing the whole componentulSelectedClass: list of selected optionsliSelectedClass: selected list itemsulOptionsClass: available options listed in the dropdown when component is in open stateliOptionClass: list items selectable from dropdown listliActiveOptionClass: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)liSelectAllClass: the "Select All" option at the top of the dropdown (when selectAllOption is enabled)liUserMsgClass: user message (last child of dropdown list when no options match user input)liActiveUserMsgClass: user message when active (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 selectedThis 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="select-all {liSelectAllClass}">Select all</li>
<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>
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) */
}
:global(div.multiselect > ul.options > li.select-all) {
/* the "Select All" option at the top of the dropdown */
}
Here are some steps to get you started if you'd like to contribute to this project!