Skip to main content

Overview

The Dropdown component provides a rich selection interface with support for single and multi-select, search/filtering, and custom rendering. Built on top of the Popover and Menu components.

Installation

import { Dropdown } from '@svelte-atoms/core';
import { filterDropdownData } from '@svelte-atoms/core';

Basic Usage

<script>
  import { Dropdown } from '@svelte-atoms/core';
  
  const data = [
    { id: 1, value: 'apple', label: 'Apple' },
    { id: 2, value: 'banana', label: 'Banana' },
    { id: 3, value: 'cherry', label: 'Cherry' }
  ];
</script>

<Dropdown.Root keys={data.map(item => item.value)}>
  {#snippet children({ dropdown })}
    <Dropdown.Trigger class="min-w-sm">
      <Dropdown.Selections />
    </Dropdown.Trigger>
    
    <Dropdown.Content>
      {#each data as item (item.id)}
        <Dropdown.Item value={item.value}>{item.label}</Dropdown.Item>
      {/each}
    </Dropdown.Content>
  {/snippet}
</Dropdown.Root>
open
boolean
Controls whether the dropdown is open
value
any
The selected value (single-select mode)
values
any[]
Array of selected values (multi-select mode)
label
string
Label for the selected item
labels
string[]
Labels for selected items
multiple
boolean
default:"false"
Enable multi-select mode
disabled
boolean
default:"false"
Disable the dropdown
placement
string
default:"bottom-start"
Popover placement position
placements
string[]
Allowed placement positions
offset
number
default:"1"
Offset distance from the trigger
keys
string[]
Array of all possible item keys for selection tracking
factory
Factory<DropdownBond>
Custom factory function for creating dropdown bond
onquerychange
(query: string) => void
Callback fired when search query changes
value
string
default:"nanoid()"
Unique identifier for this item
Custom data associated with this item
preset
string
default:"dropdown.item"
Preset key for styling
factory
() => DropdownItemController<T>
Custom factory for creating item controller
Extends MenuItem props. Extends PopoverTrigger props and all HtmlAtomProps.
class
ClassValue
CSS classes for styling the selections container
Selection
Component
Custom component for rendering each selection
getSelections
<T extends DropdownBond>(bond: T) => DropdownSelection[]
Custom function to get selections from bond
selection
DropdownSelection
required
The selection object to render
onclose
(event: Event) => void
Callback when selection is removed
Extends all HtmlAtomProps.

Subcomponents

Root container managing dropdown state. Trigger button that opens the dropdown. Popover content container for dropdown items. Individual selectable item. Container that displays selected items. Individual selected item with remove button. Placeholder text when no items are selected. Visual indicator (arrow/icon) for the dropdown state. Arrow pointing to the trigger. Scrollable list container for items. Group container for related items. Title for a group of items. Visual divider between items or groups.

Examples

<script>
  import { Dropdown, filterDropdownData } from '@svelte-atoms/core';
  import { Input } from '@svelte-atoms/core';
  import { flip } from 'svelte/animate';
  
  const data = [
    { id: 1, value: 'apple', label: 'Apple' },
    { id: 2, value: 'banana', label: 'Banana' },
    { id: 3, value: 'cherry', label: 'Cherry' },
    { id: 4, value: 'date', label: 'Date' },
    { id: 5, value: 'elderberry', label: 'Elderberry' }
  ];
  
  const filtered = filterDropdownData(
    () => data,
    (query, item) => item.label.toLowerCase().includes(query.toLowerCase())
  );
</script>

<Dropdown.Root keys={data.map(item => item.value)} multiple>
  {#snippet children({ dropdown })}
    <Dropdown.Trigger
      base={Input.Root}
      class="flex h-auto min-h-12 max-w-sm flex-col items-start gap-1 px-4"
      onclick={(ev) => {
        ev.preventDefault();
        dropdown.state.open();
      }}
    >
      <input
        class="w-full flex-1 px-1 outline-none"
        placeholder="Search for fruits..."
        bind:value={filtered.query}
      />
      
      <Dropdown.Selections class="flex flex-wrap gap-1">
        {#snippet children({ selections })}
          {#each selections as selection (selection.id)}
            <div animate:flip={{ duration: 200 }}>
              <Dropdown.Selection {selection} />
            </div>
          {/each}
        {/snippet}
      </Dropdown.Selections>
    </Dropdown.Trigger>
    
    <Dropdown.Content>
      {#each filtered.current as item (item.id)}
        <div animate:flip={{ duration: 200 }}>
          <Dropdown.Item value={item.value}>{item.label}</Dropdown.Item>
        </div>
      {/each}
    </Dropdown.Content>
  {/snippet}
</Dropdown.Root>

Single Select with Search in Content

<script>
  import { Dropdown, filterDropdownData } from '@svelte-atoms/core';
  import { Input } from '@svelte-atoms/core';
  
  const data = [
    { id: 1, value: 'apple', label: 'Apple' },
    { id: 2, value: 'banana', label: 'Banana' },
    { id: 3, value: 'cherry', label: 'Cherry' }
  ];
  
  const filtered = filterDropdownData(
    () => data,
    (query, item) => item.label.toLowerCase().includes(query.toLowerCase())
  );
</script>

<Dropdown.Root keys={data.map(item => item.value)}>
  {#snippet children({ dropdown })}
    <Dropdown.Trigger base={Input.Root} class="min-w-sm px-4">
      <Dropdown.Selections class="flex flex-wrap gap-1" />
    </Dropdown.Trigger>
    
    <Dropdown.Content>
      <input
        bind:value={filtered.query}
        class="border-b border-border px-4 py-3"
        placeholder="Search for fruits..."
      />
      
      {#each filtered.current as item (item.id)}
        <Dropdown.Item value={item.value}>{item.label}</Dropdown.Item>
      {/each}
    </Dropdown.Content>
  {/snippet}
</Dropdown.Root>

Filtering Utility

The filterDropdownData utility helps create reactive filtered lists:
<script>
  import { filterDropdownData } from '@svelte-atoms/core';
  
  const data = $state([...]);
  
  const filtered = filterDropdownData(
    () => data,
    (query, item) => item.label.toLowerCase().includes(query.toLowerCase())
  );
  
  // Use filtered.query to bind to input
  // Use filtered.current to get filtered results
</script>

Accessibility

  • Keyboard navigation with arrow keys
  • Enter/Space to select items
  • Escape to close
  • Screen reader support with ARIA attributes
  • Focus management

Build docs developers (and LLMs) love