SvMultiSelect
A multi-select dropdown: the trigger shows the picked items as chips (collapsing to "+N"), and a portalled panel holds an optional search box over a checkbox list. It picks from a fixed set - distinct from SvTagsInput, which takes free-form tags.
SvMultiSelect is the compact way to choose several values without a permanently
open list. It supports bind:value, an optional searchable box, a clearable
control, and remote loadOptions for server-backed search. Selections stay open
until the user is done, and a Done button (plus onCommit) marks the batch as
finished. Colors follow the grid's --sg-* tokens.
Related: SvListBox · SvGridSelect · Selection overview
Installation
Add it with the CLI - this drops a ready-to-edit SvMultiSelect starter into your app:
Prefer to see it first? npx @svgrid/ui try multi-select opens it in a throwaway sandbox - no project needed.
Or install the package and import it directly. SvMultiSelect ships free in
@svgrid/grid and is part of the grid's editor kit - the same control SvGrid mounts when you edit a matching cell:
import { SvMultiSelect } from '@svgrid/grid'
Example
<script lang="ts">
import { SvMultiSelect, type MultiSelectOption } from '@svgrid/grid'
const options: MultiSelectOption[] = [
{ value: 'ts', label: 'TypeScript' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'rust', label: 'Rust' },
]
let value = $state<Array<string | number>>([])
</script>
<SvMultiSelect label="Skills" {options} bind:value maxTagCount={2} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options |
ReadonlyArray<MultiSelectOption> |
- | The options. When loadOptions is set, this is the seed list. |
value |
ReadonlyArray<string | number> |
[] |
Selected values. Bindable (bind:value). |
onChange |
(value: Array<string | number>) => void |
- | Fires whenever the selection changes. |
onCommit |
(value: Array<string | number>) => void |
- | Fires when the user presses Done. |
onCancel |
() => void |
- | Fires when the panel is dismissed without committing. |
placeholder |
string |
'Select…' |
Shown when nothing is selected. |
searchable |
boolean |
true |
Show a search box above the list. |
maxTagCount |
number |
3 |
Chips shown in the trigger before collapsing to "+N". |
clearable |
boolean |
true |
Show a clear-all control when there is a selection. |
loadOptions |
(query: string) => Promise<MultiSelectOption[]> |
- | Remote, debounced search that replaces the list. |
debounceMs |
number |
250 |
Debounce for remote search. |
loadingText |
string |
'Loading…' |
Text shown while a remote search runs. |
size |
sm | md | lg |
md |
Trigger height and font size. |
disabled |
boolean |
false |
Blocks interaction. |
label |
string |
- | Visible field label, wired to the control. |
hint |
string |
- | Helper text under the control. |
error |
string |
- | Error message; announced and styled when set. |
required |
boolean |
false |
Marks the field required. |
invalid |
boolean |
false |
Applies the invalid state. |
name |
string |
- | Emits hidden input(s) carrying each value for form posts. |
dir |
ltr | rtl | auto |
auto |
Text direction. |
ariaLabel |
string |
- | Accessible name when there is no visible label. |
id |
string |
- | Root id; label/hint/error ids derive from it. |
MultiSelectOption
type MultiSelectOption = {
value: string | number
label: string
disabled?: boolean
}
Examples
Two-way binding
Bind the array directly and read it anywhere - no onChange wiring needed:
<SvMultiSelect {options} bind:value={selected} />
<p>{selected.length} chosen</p>
Remote options
Give loadOptions an async function; the seed options resolve chip labels for
values that are not in the current result page:
<SvMultiSelect label="Tags" options={recent}
loadOptions={async (q) => (await fetch(`/api/tags?q=${q}`)).json()}
bind:value />
Commit on Done
Use onCommit to run work only once the user finishes picking, rather than on
every toggle:
<SvMultiSelect {options} bind:value onCommit={(v) => saveFilters(v)} />
As a grid cell editor
Register SvMultiSelect under a type name and a column can edit an array-valued
cell (tags, skills) in place. Map the grid's edit context onto the props:
<script lang="ts">
import { SvGrid, SvMultiSelect, registerCellEditor,
type MultiSelectOption } from '@svgrid/grid'
const skills: MultiSelectOption[] = [
{ value: 'ts', label: 'TypeScript' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'rust', label: 'Rust' },
]
registerCellEditor('skills', {
component: SvMultiSelect,
props: (ctx) => ({
options: skills,
value: (ctx.value as Array<string | number>) ?? [],
onChange: ctx.onChange,
onCommit: (v: Array<string | number>) => ctx.onCommit(v),
}),
})
const columns = [{ field: 'skills', header: 'Skills', editorType: 'skills' }]
</script>
Every toggle flows through onChange; onCommit ends the edit with the final
array when the user presses Done.
Tip: set
disabledon aMultiSelectOptionto keep it visible but unpickable - toggling ignores disabled rows rather than hiding them.
Accessibility
- The trigger carries
aria-haspopup/aria-expanded; the panel list is arole="listbox"witharia-multiselectableandaria-selectedper option. - The search box is a
role="combobox"driving the list viaaria-activedescendant; arrow keys move,Enter/Spacetoggle,Escapedismisses. label,hint, anderrorare wired viaaria-describedby; passariaLabelwhen there is no visible label.
Sizes
Every control takes the same three sizes, so a dense toolbar and a roomy form can share components.
<script lang="ts">
import { SvMultiSelect } from '@svgrid/grid'
let selected = $state<string[]>([])
</script>
<SvMultiSelect bind:value={selected} size="sm" />
<SvMultiSelect bind:value={selected} size="md" />
<SvMultiSelect bind:value={selected} size="lg" />
In a form
The shared field props behave the same on every editor: label names it, hint explains it, and error plus invalid mark it - which is why a validated form does not need per-component handling.
<script lang="ts">
import { SvMultiSelect } from '@svgrid/grid'
let selected = $state<string[]>([])
</script>
<SvMultiSelect
bind:value={selected}
label="Label"
hint="A short hint"
required
/>
<SvMultiSelect
bind:value={selected}
label="Label"
error="Something is wrong"
invalid
/>
Tag overflow and clearing
maxTagCount decides when the chips collapse into a "+N" counter, which is
what keeps a multi-select from growing taller than the field around it.
clearable adds the escape hatch for a selection someone regrets.
<script lang="ts">
import { SvMultiSelect } from '@svgrid/grid'
import type { ListOption } from '@svgrid/grid'
const options: ListOption[] = [
{ value: 'svelte', label: 'Svelte' },
{ value: 'grid', label: 'Data grid' },
{ value: 'a11y', label: 'Accessibility' },
{ value: 'perf', label: 'Performance' },
{ value: 'i18n', label: 'Internationalisation' },
]
let picked = $state<string[]>(['svelte', 'grid', 'a11y'])
</script>
<SvMultiSelect
{options}
value={picked}
onChange={(v) => (picked = v as string[])}
label="Topics"
searchable
clearable
maxTagCount={2}
/>
See also
- Selection overview - the whole picker family at a glance.
- SvListBox - inline multi-select without a popover.
- SvGridSelect - pick a row by more than one column.