svelte-multiselect Svelte MultiSelect

« home

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';
// Simulated large dataset - in practice, this would be a database/API
const all_items = Array.from({ length: 10000 }, (_, idx) => `Item ${idx + 1}`);
async function load_options({ search, offset, limit }) {
    // 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';
// Simulated API
async function fetch_users({ search, offset, limit }) {
    await new Promise((resolve) => setTimeout(resolve, 400));
    // Simulated database
    const all_users = 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';
const all_items = Array.from({ length: 500 }, (_, idx) => `Option ${idx + 1}`);
async function load_options({ search, offset, limit }) {
    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';
const all_languages = [
    { 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 }) {
    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';
const items = Array.from({ length: 100 }, (_, idx) => `Item ${idx + 1}`);
async function load_options({ search, offset, limit }) {
    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 = $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.