The Future of Web Dev
The Future of Web Dev
Custom Select and Autocomplete for Svelte Apps – Svelte Select
Add searchable select inputs to Svelte apps with single and multiple selection, async options, grouped items, snippets, callbacks, and CSS variables.

Svelte Select is a Svelte component that creates searchable select, autocomplete, and typeahead controls in your Svelte apps.
It works with single values, multiple selections, grouped options, remote data, custom item markup, and form-friendly primitive IDs.
Key Features
- Filters option lists as the user types.
- Switches between single-select and multi-select behavior.
- Loads remote options through a debounced
loadOptionsfunction. - Groups related entries and marks individual rows as non-selectable.
- Stores either complete item objects or primitive IDs through
valueMode. - Replaces option rows, selection markup, icons, empty states, and list regions with Svelte snippets.
- Positions the dropdown through Floating UI configuration.
Use Cases
- Product and customer forms with long option lists.
- Admin filters with several statuses, categories, team members, or labels.
- Customer, repository, city, or inventory lookups.
How To Use Svelte Select
Installation
npm install svelte-selectBasic Usage
Pass an array through items and bind the selected item through value. Object items use value and label fields by default.
<script>
import Select from 'svelte-select';
const frameworks = [
{ value: 'svelte', label: 'Svelte' },
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'solid', label: 'Solid' }
];
let selectedFramework = $state();
</script>
<Select
items={frameworks}
bind:value={selectedFramework}
placeholder="Choose a framework"
/>
{#if selectedFramework}
<p>Selected: {selectedFramework.label}</p>
{/if}Choose the Shape of value
valueMode controls the value stored by bind:value. Object collections default to the complete selected item. Set valueMode="id" when a form or API needs only the identifier.
Primitive arrays already bind primitive values and do not need valueMode="id". For object collections, keep value, valueMode, and itemId aligned.
| Mode | Single Selection | Multiple Selection | Stored Value |
|---|---|---|---|
item | Item object | Item object array | Complete selected item |
id | Primitive ID | Primitive ID array | item[itemId] |
Primitive items | Primitive | Primitive array | Matching primitive |
For a form that submits a plan ID:
<script>
import Select from 'svelte-select';
const plans = [
{ value: 'starter', label: 'Starter' },
{ value: 'team', label: 'Team' },
{ value: 'business', label: 'Business' }
];
let selectedPlan = $state('starter');
</script>
<Select
items={plans}
valueMode="id"
bind:value={selectedPlan}
name="plan"
required
/>Custom object fields use itemId and label:
<script>
import Select from 'svelte-select';
const projects = [
{ id: 101, title: 'Marketing Site' },
{ id: 102, title: 'Admin Dashboard' },
{ id: 103, title: 'Documentation' }
];
let projectId = $state(101);
</script>
<Select
items={projects}
itemId="id"
label="title"
valueMode="id"
bind:value={projectId}
/>Multiple and Grouped Options
Set multiple for multi-select input. A groupBy function assigns each row to a group, while selectable: false keeps a row visible but prevents selection.
<script>
import Select from 'svelte-select';
const technologies = [
{ value: 'svelte', label: 'Svelte', group: 'Frontend' },
{ value: 'vue', label: 'Vue', group: 'Frontend' },
{ value: 'node', label: 'Node.js', group: 'Backend' },
{ value: 'postgres', label: 'PostgreSQL', group: 'Database' },
{
value: 'legacy',
label: 'Legacy Platform',
group: 'Backend',
selectable: false
}
];
const groupBy = (item) => item.group;
let selectedTechnologies = $state([]);
</script>
<Select
items={technologies}
{groupBy}
multiple
bind:value={selectedTechnologies}
placeholder="Select technologies"
/>Load Options Asynchronously
loadOptions receives the current filter text and returns a Promise that resolves to an item array. The component debounces calls internally through debounceWait, whose default value is 300 milliseconds.
<script>
import Select from 'svelte-select';
async function searchCustomers(filterText) {
const response = await fetch(
`/api/customers?q=${encodeURIComponent(filterText)}`
);
const customers = await response.json();
return customers.map((customer) => ({
value: customer.id,
label: customer.name
}));
}
let customerId = $state();
</script>
<Select
loadOptions={searchCustomers}
valueMode="id"
bind:value={customerId}
debounceWait={400}
placeholder="Search customers"
/>Customize Option Markup with Snippets
Svelte Select uses snippet props for custom rendering. The item snippet receives the current item and index. Other snippets target selections, icons, list regions, empty states, hidden inputs, and the required indicator.
<script>
import Select from 'svelte-select';
const members = [
{ value: 'ava', label: 'Ava Carter', role: 'Designer' },
{ value: 'liam', label: 'Liam Reed', role: 'Developer' },
{ value: 'mia', label: 'Mia Brooks', role: 'Product Manager' }
];
</script>
<Select items={members} placeholder="Assign a team member">
{#snippet item({ item })}
<div class="member-option">
<strong>{item.label}</strong>
<span>{item.role}</span>
</div>
{/snippet}
</Select>
<style>
.member-option {
display: grid;
gap: 0.125rem;
}
.member-option span {
font-size: 0.875rem;
opacity: 0.7;
}
</style>Available Component Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
items | any[] | [] | Option data available for filtering and selection. |
value | any | undefined | Current selected value or values. Its shape follows valueMode. |
valueMode | string | item | Stores complete items with item or primitive identifiers with id. |
itemId | string | value | Sets the object field used as the option identifier. |
label | string | label | Sets the object field used as the visible label. |
id | string | null | Sets the input element ID. |
filterText | string | '' | Current text used to filter items. |
placeholder | string | Please select | Sets the placeholder text. |
hideEmptyState | boolean | false | Hides the list when filtering returns no items. |
listOpen | boolean | false | Controls the open state of the option list. |
class | string | '' | Adds classes to the component container. |
containerStyles | string | '' | Adds inline styles to the container. |
clearable | boolean | true | Activates value clearing. |
disabled | boolean | false | Disables the select control. |
multiple | boolean | false | Activates multiple selection. |
searchable | boolean | true | Activates text search and filtering. |
groupHeaderSelectable | boolean | false | Makes group headers selectable. |
focused | boolean | false | Controls input focus state. |
listAutoWidth | boolean | true | Controls automatic list width behavior. |
showChevron | boolean | false | Displays the dropdown chevron. |
inputAttributes | object | {} | Passes HTML attributes to the internal input. |
placeholderAlwaysShow | boolean | false | Keeps placeholder text visible in multi-select mode. |
loading | boolean | false | Controls the loading indicator when loadOptions does not set it. |
listOffset | number | 5 | Sets the pixel gap between the control and dropdown list. |
debounceWait | number | 300 | Sets the debounce delay in milliseconds. |
floatingConfig | object | {} | Passes configuration to Floating UI positioning. |
hasError | boolean | false | Applies the component’s error state and styles. |
name | string | null | Names the hidden input used with forms. |
required | boolean | false | Blocks form submission when the field has no required value. |
multiFullItemClearable | boolean | false | Removes a selected multi-select item when its full item region is clicked. |
closeListOnChange | boolean | true | Closes the option list after onchange. |
clearFilterTextOnBlur | boolean | true | Clears the current filter when the input loses focus. |
Bindable Values
| Bindable Value | Role |
|---|---|
value | Current selection. |
filterText | Search text. |
items | Current item collection. |
loading | Loading state. |
listOpen | Dropdown open state. |
focused | Input focus state. |
hoverItemIndex | Current hovered item index. |
container | Component container reference. |
input | Input element reference. |
Callback Props
| Callback | Payload | Trigger |
|---|---|---|
onchange | value | The user selects an option. |
oninput | value | The bound value changes. |
onselect | selection | An option is selected. |
onfocus | FocusEvent | The input receives focus. |
onblur | FocusEvent | The input loses focus. |
onclear | value or removed item | A value is cleared or a multi-select item is removed. |
onloaded | { items } | loadOptions resolves. |
onerror | { type, details } | An internal async operation reports an error. |
onfilter | filteredItems | Items are filtered while the list is open. |
onhoverItem | hoverItemIndex | The hovered list item changes. |
A callback reads its payload directly:
<script>
import Select from 'svelte-select';
const categories = [
{ value: 'frontend', label: 'Frontend' },
{ value: 'backend', label: 'Backend' }
];
function handleCategoryChange(value) {
console.log(value);
}
</script>
<Select
items={categories}
onchange={handleCategoryChange}
/>Snippets
| Snippet | Parameters | Target |
|---|---|---|
prepend | None | Content before the main select content. |
selection | { selection, index } | Selected item markup. index exists in multi-select mode. |
clearIcon | None | Single-value clear icon. |
multiClearIcon | None | Clear icon attached to a multi-select item. |
loadingIcon | None | Loading indicator. |
chevronIcon | { listOpen } | Dropdown chevron. |
listPrepend | None | Content at the start of the option list. |
list | { filteredItems } | Entire filtered list rendering. |
listAppend | None | Content at the end of the option list. |
item | { item, index } | Individual option row. |
empty | None | Empty-result content. |
inputHidden | { value } | Hidden form input. |
requiredIndicator | { value } | Required-field indicator. |
Advanced API
Filtering, Grouping, and Loading Overrides
| Prop | Role |
|---|---|
itemFilter | Defines how an individual label matches filterText. |
groupBy | Returns the group assigned to an item. |
groupFilter | Changes the generated group collection. |
createGroupHeaderItem | Builds the item used for a group header. |
loadOptions | Returns a Promise that resolves to option data. |
debounce | Replaces the debounce implementation. |
filter | Replaces the component’s filtering function. |
getItems | Replaces the item retrieval pipeline. |
Imperative Methods
| Method | Result |
|---|---|
getFilteredItems() | Returns the current filtered item list. |
handleClear() | Clears the current selection and focuses the input. |
<script>
import Select from 'svelte-select';
const items = ['Svelte', 'Vue', 'React'];
let selectRef;
</script>
<Select bind:this={selectRef} {items} />
<button type="button" onclick={() => selectRef.handleClear()}>
Clear selection
</button>ARIA Text Hooks
| Prop | Purpose |
|---|---|
ariaValues | Formats selected-value text. |
ariaListOpen | Formats the message for the focused option and result count. |
ariaFocused | Formats the message announced when the select receives focus. |
Styling and Theming
The component exposes CSS custom properties for the container, input, placeholder, dropdown list, options, grouped items, selected values, icons, loading indicator, disabled state, and error state. Current variables use kebab-case names such as --border-radius, --placeholder-color, --item-hover-bg, --list-max-height, and --multi-item-bg.
Set variables directly on the component:
<script>
import Select from 'svelte-select';
const items = ['Design', 'Development', 'Marketing'];
</script>
<Select
{items}
--border-radius="8px"
--placeholder-color="#6b7280"
--list-max-height="260px"
/>inputStyles applies inline style overrides to the internal input:
<Select
{items}
inputStyles="box-sizing: border-box;"
/>Replace the Default Styles
The package exports an unstyled component and an experimental Tailwind stylesheet. The Tailwind stylesheet uses @extend, which requires PostCSS.
import Select from 'svelte-select/no-styles';
import 'svelte-select/tailwind.css';Implementation Notes
- Keep object
items,value,itemId, andvalueModein the same data shape. The component does not convert mismatched object and primitive values in v6. - Primitive item arrays keep primitive bound values with the default
valueMode.['Svelte', 'Vue']therefore binds'Svelte'directly. valueMode="id"writes primitive identifiers for object collections. It also changes the payload shape received byonchangeandoninput.- Give externally bound props a defined initial value when the component declares its own default. Svelte 5 rejects conflicting undefined bindings for these defaults.
- List positioning relies on Floating UI. Rollup and low/no-build configurations that fail on package resolution need the Floating UI package-entry-point configuration checked.
floatingConfigaccepts Floating UI positioning configuration.strategy: 'fixed'is available for layouts where the dropdown needs fixed positioning.
Alternatives and Related Resources
FAQs
Q: How do I get only an option ID from Svelte Select?
A: Use object items with valueMode="id". bind:value then stores item[itemId], where itemId defaults to value.
Q: Why does my selected value have the wrong shape?
A: Match value to the active valueMode. Object items in item mode use complete objects, while id mode uses primitive identifiers.
Q: How do I customize an option row in Svelte?
A: Add an item snippet and read its { item, index } parameters. Version 6 replaces the older named-slot API with snippet props.
Q: How do I use Svelte Select with custom styles or Tailwind CSS?
A: Import svelte-select/no-styles for the unstyled component. The package also exports the experimental svelte-select/tailwind.css stylesheet, which requires PostCSS for its @extend rules.
Q: Why does bind:value stay undefined after selecting an item with valueMode=”id”?
A: Check that itemId matches a real key on the items objects. With valueMode="id", value is set from item[itemId], not the whole item.
Q: How do I stop the list from closing after each pick in a multi-select?
A: Set closeListOnChange={false} on the Select.





