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 loadOptions function.
  • 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-select

Basic 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.

ModeSingle SelectionMultiple SelectionStored Value
itemItem objectItem object arrayComplete selected item
idPrimitive IDPrimitive ID arrayitem[itemId]
Primitive itemsPrimitivePrimitive arrayMatching 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

PropTypeDefaultPurpose
itemsany[][]Option data available for filtering and selection.
valueanyundefinedCurrent selected value or values. Its shape follows valueMode.
valueModestringitemStores complete items with item or primitive identifiers with id.
itemIdstringvalueSets the object field used as the option identifier.
labelstringlabelSets the object field used as the visible label.
idstringnullSets the input element ID.
filterTextstring''Current text used to filter items.
placeholderstringPlease selectSets the placeholder text.
hideEmptyStatebooleanfalseHides the list when filtering returns no items.
listOpenbooleanfalseControls the open state of the option list.
classstring''Adds classes to the component container.
containerStylesstring''Adds inline styles to the container.
clearablebooleantrueActivates value clearing.
disabledbooleanfalseDisables the select control.
multiplebooleanfalseActivates multiple selection.
searchablebooleantrueActivates text search and filtering.
groupHeaderSelectablebooleanfalseMakes group headers selectable.
focusedbooleanfalseControls input focus state.
listAutoWidthbooleantrueControls automatic list width behavior.
showChevronbooleanfalseDisplays the dropdown chevron.
inputAttributesobject{}Passes HTML attributes to the internal input.
placeholderAlwaysShowbooleanfalseKeeps placeholder text visible in multi-select mode.
loadingbooleanfalseControls the loading indicator when loadOptions does not set it.
listOffsetnumber5Sets the pixel gap between the control and dropdown list.
debounceWaitnumber300Sets the debounce delay in milliseconds.
floatingConfigobject{}Passes configuration to Floating UI positioning.
hasErrorbooleanfalseApplies the component’s error state and styles.
namestringnullNames the hidden input used with forms.
requiredbooleanfalseBlocks form submission when the field has no required value.
multiFullItemClearablebooleanfalseRemoves a selected multi-select item when its full item region is clicked.
closeListOnChangebooleantrueCloses the option list after onchange.
clearFilterTextOnBlurbooleantrueClears the current filter when the input loses focus.

Bindable Values

Bindable ValueRole
valueCurrent selection.
filterTextSearch text.
itemsCurrent item collection.
loadingLoading state.
listOpenDropdown open state.
focusedInput focus state.
hoverItemIndexCurrent hovered item index.
containerComponent container reference.
inputInput element reference.

Callback Props

CallbackPayloadTrigger
onchangevalueThe user selects an option.
oninputvalueThe bound value changes.
onselectselectionAn option is selected.
onfocusFocusEventThe input receives focus.
onblurFocusEventThe input loses focus.
onclearvalue or removed itemA value is cleared or a multi-select item is removed.
onloaded{ items }loadOptions resolves.
onerror{ type, details }An internal async operation reports an error.
onfilterfilteredItemsItems are filtered while the list is open.
onhoverItemhoverItemIndexThe 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

SnippetParametersTarget
prependNoneContent before the main select content.
selection{ selection, index }Selected item markup. index exists in multi-select mode.
clearIconNoneSingle-value clear icon.
multiClearIconNoneClear icon attached to a multi-select item.
loadingIconNoneLoading indicator.
chevronIcon{ listOpen }Dropdown chevron.
listPrependNoneContent at the start of the option list.
list{ filteredItems }Entire filtered list rendering.
listAppendNoneContent at the end of the option list.
item{ item, index }Individual option row.
emptyNoneEmpty-result content.
inputHidden{ value }Hidden form input.
requiredIndicator{ value }Required-field indicator.

Advanced API

Filtering, Grouping, and Loading Overrides

PropRole
itemFilterDefines how an individual label matches filterText.
groupByReturns the group assigned to an item.
groupFilterChanges the generated group collection.
createGroupHeaderItemBuilds the item used for a group header.
loadOptionsReturns a Promise that resolves to option data.
debounceReplaces the debounce implementation.
filterReplaces the component’s filtering function.
getItemsReplaces the item retrieval pipeline.

Imperative Methods

MethodResult
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

PropPurpose
ariaValuesFormats selected-value text.
ariaListOpenFormats the message for the focused option and result count.
ariaFocusedFormats 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, and valueMode in 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 by onchange and oninput.
  • 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.
  • floatingConfig accepts 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.

rob-balfre

rob-balfre

Leave a Reply

Your email address will not be published. Required fields are marked *