svelte-multiselect Svelte MultiSelect

Dynamic Options Loading

For large datasets or server-side data, use loadOptions to dynamically load options as the user scrolls and searches. The component handles all state management, debouncing, and pagination automatically.

This addresses GitHub discussion #342.

Basic Example

Just provide a loadOptions function that fetches data:

<script lang="ts">
  import MultiSelect from '$lib'
  import type { LoadOptionsParams, LoadOptionsResult } from '$lib/types'

  // Simulated large dataset - in practice, this would be a database/API
  const all_items: string[] = Array.from(
    { length: 10000 },
    (_, idx) => `Item ${idx + 1}`,
  )

  async function load_options(
    { search, offset, limit }: LoadOptionsParams,
  ): Promise<LoadOptionsResult<string>> {
    // Simulate network delay
    await new Promise((resolve) => setTimeout(resolve, 300))

    // Filter and paginate
    const filtered = search
      ? all_items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
      : all_items

    return {
      options: filtered.slice(offset, offset + limit),
      hasMore: offset + limit < filtered.length,
    }
  }
</script>

<MultiSelect loadOptions={load_options} placeholder="Search 10,000 items..." />

That’s it! No state management needed. The component handles:

REST API Example

Connect to a real API:

<script lang="ts">
  import MultiSelect from '$lib'
  import type { LoadOptionsParams, LoadOptionsResult, ObjectOption } from '$lib/types'

  interface User extends ObjectOption {
    email: string
    id: number
  }

  // Simulated API
  async function fetch_users(
    { search, offset, limit }: LoadOptionsParams,
  ): Promise<LoadOptionsResult<User>> {
    await new Promise((resolve) => setTimeout(resolve, 400))

    // Simulated database
    const all_users: User[] = Array.from({ length: 2000 }, (_, idx) => ({
      label: `User ${idx + 1}`,
      email: `user${idx + 1}@example.com`,
      id: idx + 1,
    }))

    const filtered = search
      ? all_users.filter((user) =>
        user.label.toLowerCase().includes(search.toLowerCase()) ||
        user.email.toLowerCase().includes(search.toLowerCase())
      )
      : all_users

    return {
      options: filtered.slice(offset, offset + limit),
      hasMore: offset + limit < filtered.length,
    }
  }
</script>

<MultiSelect
  loadOptions={fetch_users}
  placeholder="Search users by name or email..."
>
  {#snippet children({ option })}
    <div>
      <strong>{option.label}</strong>
      <small style="opacity: 0.7; margin-left: 8px">{option.email}</small>
    </div>
  {/snippet}
</MultiSelect>

Configuration Options

For advanced control, pass an object with fetch function and config:

<script lang="ts">
  import MultiSelect from '$lib'
  import type { LoadOptionsParams, LoadOptionsResult } from '$lib/types'

  const all_items: string[] = Array.from(
    { length: 500 },
    (_, idx) => `Option ${idx + 1}`,
  )

  async function load_options(
    { search, offset, limit }: LoadOptionsParams,
  ): Promise<LoadOptionsResult<string>> {
    await new Promise((resolve) => setTimeout(resolve, 200))

    const filtered = search
      ? all_items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
      : all_items

    return {
      options: filtered.slice(offset, offset + limit),
      hasMore: offset + limit < filtered.length,
    }
  }
</script>

<MultiSelect
  loadOptions={{ fetch: load_options, debounceMs: 500, batchSize: 20 }}
  placeholder="Custom config (500ms debounce, 20 items per batch)"
/>

Object Options with Custom Display

Use object options with custom snippets:

<script lang="ts">
  import MultiSelect from '$lib'
  import type { LoadOptionsParams, LoadOptionsResult, ObjectOption } from '$lib/types'

  interface Language extends ObjectOption {
    year: number
  }

  const all_languages: Language[] = [
    { label: `JavaScript`, year: 1995 },
    { label: `TypeScript`, year: 2012 },
    { label: `Python`, year: 1991 },
    { label: `Rust`, year: 2010 },
    { label: `Go`, year: 2009 },
    { label: `Java`, year: 1995 },
    { label: `C++`, year: 1985 },
    { label: `C#`, year: 2000 },
    { label: `Ruby`, year: 1995 },
    { label: `Swift`, year: 2014 },
  ]

  async function load_options(
    { search, offset, limit }: LoadOptionsParams,
  ): Promise<LoadOptionsResult<Language>> {
    await new Promise((resolve) => setTimeout(resolve, 200))

    const filtered = search
      ? all_languages.filter((lang) =>
        lang.label.toLowerCase().includes(search.toLowerCase())
      )
      : all_languages

    return {
      options: filtered.slice(offset, offset + limit),
      hasMore: offset + limit < filtered.length,
    }
  }
</script>

<MultiSelect loadOptions={load_options} placeholder="Search languages...">
  {#snippet children({ option })}
    <span>{option.label} <small style="opacity: 0.6">({option.year})</small></span>
  {/snippet}
</MultiSelect>

Lazy Loading on Open

By default, options load when the dropdown opens. Set onOpen: false to disable:

Selected: none

<script lang="ts">
  import MultiSelect from '$lib'
  import type { LoadOptionsParams, LoadOptionsResult } from '$lib/types'

  const items: string[] = Array.from({ length: 100 }, (_, idx) => `Item ${idx + 1}`)

  async function load_options(
    { search, offset, limit }: LoadOptionsParams,
  ): Promise<LoadOptionsResult<string>> {
    await new Promise((resolve) => setTimeout(resolve, 300))
    const filtered = search
      ? items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
      : items
    return {
      options: filtered.slice(offset, offset + limit),
      hasMore: offset + limit < filtered.length,
    }
  }

  let selected: string[] = $state([])
</script>

<MultiSelect
  loadOptions={{ fetch: load_options, onOpen: false }}
  bind:selected
  placeholder="Type to search (won't load on open)..."
/>

<p>Selected: {selected.join(`, `) || `none`}</p>

Props Reference

The loadOptions prop accepts either a function (simple) or an object (with config):

// Simple: just a function
loadOptions={myFetchFn}

// With config: object with fetch + options
loadOptions={{ fetch: myFetchFn, debounceMs: 500, batchSize: 20, onOpen: false }}
Config KeyTypeDefaultDescription
fetchfnAsync function to load options (required)
debounceMsnumber300Debounce delay for search queries
batchSizenumber50Number of options to load per batch
onOpenbooleantrueWhether to load options when dropdown opens

LoadOptions Parameters

interface LoadOptionsParams {
  search: string // Current search text
  offset: number // Number of options already loaded (for pagination)
  limit: number // Batch size to load
}

interface LoadOptionsResult<T> {
  options: T[] // Array of options to add
  hasMore: boolean // Whether more options are available
}

Error Handling

If loadOptions throws or rejects, the error is logged to console and the component continues to function normally. The loading indicator will be hidden and users can retry by typing or scrolling.