Forms

SelectMenu

A searchable dropdown select with icons, avatars, grouped items, descriptions, and custom rendering. Built on bits-ui Combobox with full keyboard navigation.

Playground

Experiment with different props in real-time.

Basic Usage

Pass items and use bind:value. Items are searchable by default.

<script lang="ts">
  import { SelectMenu } from 'sv5ui';

  let value = $state('');

  const items = [
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'orange', label: 'Orange' },
    { value: 'grape', label: 'Grape' },
    { value: 'mango', label: 'Mango' }
  ];
</script>

<SelectMenu {items} bind:value placeholder="Select a fruit..." />

With Icons

Add Iconify icons to items.

<SelectMenu
  placeholder="Select status..."
  items={[
    { value: 'active', label: 'Active', icon: 'lucide:check-circle' },
    { value: 'paused', label: 'Paused', icon: 'lucide:pause-circle' },
    { value: 'closed', label: 'Closed', icon: 'lucide:x-circle' }
  ]}
/>

With Avatar

Display avatars alongside items.

<SelectMenu
  placeholder="Assign to..."
  items={[
    { value: 'john', label: 'John Doe', avatar: { src: 'https://i.pravatar.cc/40?u=1', alt: 'John' } },
    { value: 'jane', label: 'Jane Smith', avatar: { src: 'https://i.pravatar.cc/40?u=2', alt: 'Jane' } },
    { value: 'bob', label: 'Bob Wilson', avatar: { src: 'https://i.pravatar.cc/40?u=3', alt: 'Bob' } }
  ]}
/>

With Description

Add secondary text to items.

<SelectMenu
  placeholder="Select a plan..."
  items={[
    { value: 'free', label: 'Free', description: '5GB storage, basic features' },
    { value: 'pro', label: 'Pro', description: '50GB storage, advanced analytics' },
    { value: 'enterprise', label: 'Enterprise', description: 'Unlimited storage, priority support' }
  ]}
/>

Grouped Items

Use type: 'label' for headings and type: 'separator' for dividers.

<SelectMenu
  placeholder="Select a framework..."
  items={[
    { type: 'label', label: 'Frontend' },
    { value: 'svelte', label: 'Svelte', icon: 'logos:svelte-icon' },
    { value: 'react', label: 'React', icon: 'logos:react' },
    { value: 'vue', label: 'Vue', icon: 'logos:vue' },
    { type: 'separator' },
    { type: 'label', label: 'Backend' },
    { value: 'node', label: 'Node.js', icon: 'logos:nodejs-icon' },
    { value: 'python', label: 'Python', icon: 'logos:python' },
    { value: 'go', label: 'Go', icon: 'logos:go' }
  ]}
/>

Variants

4 visual styles for the trigger.

<SelectMenu variant="outline" placeholder="Outline" {items} />
<SelectMenu variant="soft" placeholder="Soft" {items} />
<SelectMenu variant="subtle" placeholder="Subtle" {items} />
<SelectMenu variant="ghost" placeholder="Ghost" {items} />

Sizes

5 sizes.

<SelectMenu size="xs" placeholder="Extra Small" {items} />
<SelectMenu size="sm" placeholder="Small" {items} />
<SelectMenu size="md" placeholder="Medium" {items} />
<SelectMenu size="lg" placeholder="Large" {items} />
<SelectMenu size="xl" placeholder="Extra Large" {items} />

Colors

Visible with highlight active.

<SelectMenu color="primary" highlight placeholder="Primary" {items} />
<SelectMenu color="success" highlight placeholder="Success" {items} />
<SelectMenu color="warning" highlight placeholder="Warning" {items} />
<SelectMenu color="error" highlight placeholder="Error" {items} />

Search & Filtering

Items are searchable by default. Customize with filterFields or disable with ignoreFilter.

<!-- Default search (searches label and value) -->
<SelectMenu placeholder="Search items..." {items} />

<!-- Custom search fields -->
<SelectMenu
  placeholder="Search by description..."
  filterFields={['label', 'description']}
  items={[
    { value: 'free', label: 'Free', description: '5GB storage' },
    { value: 'pro', label: 'Pro', description: '50GB storage' },
    { value: 'enterprise', label: 'Enterprise', description: 'Unlimited' }
  ]}
/>

<!-- Disable filtering (server-side) -->
<SelectMenu placeholder="Server-side..." ignoreFilter {items} />

Loading

Show a spinner.

<SelectMenu loading placeholder="Loading..." {items} />

Disabled

Disable the entire menu or individual items.

<SelectMenu disabled placeholder="Disabled" {items} />

<!-- Individual disabled items -->
<SelectMenu
  placeholder="Some items disabled"
  items={[
    { value: 'active', label: 'Active' },
    { value: 'disabled', label: 'Disabled option', disabled: true },
    { value: 'another', label: 'Another active' }
  ]}
/>

Controlled

Use bind:value for two-way binding.

Selected: none

<script lang="ts">
  import { SelectMenu, Button } from 'sv5ui';

  let value = $state('');
</script>

<SelectMenu {items} bind:value placeholder="Pick one..." />
<p>Selected: {value || 'none'}</p>
<Button size="sm" variant="outline" label="Reset" onclick={() => value = ''} />

Multiple Selection

Set multiple to pick more than one option. value becomes string[], the dropdown stays open after each pick, and labels are joined by separator (default ", ").

<script lang="ts">
  import { SelectMenu } from 'sv5ui';

  let value = $state<string[]>(['apple', 'banana']);

  const fruits = [
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'orange', label: 'Orange' },
    { value: 'grape', label: 'Grape' },
    { value: 'mango', label: 'Mango' }
  ];
</script>

<!-- Default separator: ", " -->
<SelectMenu items={fruits} bind:value multiple placeholder="Pick fruits..." />

<!-- Custom separator (note: SelectMenu has no defaultValue prop — initialize via state) -->
<SelectMenu items={fruits} bind:value multiple separator=" • " placeholder="Pick fruits..." />

Multiple with Chips

Use the selected snippet to render selected values as chips. It receives { items, remove, clear } — call stopPropagation on remove clicks so the trigger doesn't toggle.

<script lang="ts">
  import { SelectMenu, Badge } from 'sv5ui';

  let value = $state<string[]>(['svelte', 'node']);

  const items = [
    { type: 'label', label: 'Frontend' },
    { value: 'svelte', label: 'Svelte', icon: 'logos:svelte-icon' },
    { value: 'react', label: 'React', icon: 'logos:react' },
    { value: 'vue', label: 'Vue', icon: 'logos:vue' },
    { type: 'separator' },
    { type: 'label', label: 'Backend' },
    { value: 'node', label: 'Node.js', icon: 'logos:nodejs-icon' },
    { value: 'python', label: 'Python', icon: 'logos:python' },
    { value: 'go', label: 'Go', icon: 'logos:go' }
  ];
</script>

<SelectMenu {items} bind:value multiple placeholder="Pick technologies...">
  {#snippet selected({ items, remove })}
    <div class="flex flex-wrap items-center gap-1">
      {#each items as item (item.value)}
        <Badge
          label={item.label}
          variant="soft"
          size="sm"
          trailingIcon="lucide:x"
          class="cursor-pointer"
          onclick={(e) => { e.stopPropagation(); remove(item.value); }}
        />
      {/each}
    </div>
  {/snippet}
</SelectMenu>

Create New Items

Set createItem to let users add new values by typing. 'lazy' (default when true) only shows the create option when nothing matches; 'always' keeps it visible as long as the search term is non-empty. Pressing Enter from the search input creates when there are no matches.

<script lang="ts">
  import { SelectMenu } from 'sv5ui';

  let lazyValue = $state('');
  let alwaysValue = $state('');

  const fruits = [
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'orange', label: 'Orange' }
  ];
</script>

<!-- 'lazy' (default when createItem={true}): only shown when no match -->
<SelectMenu
  items={fruits}
  bind:value={lazyValue}
  createItem="lazy"
  createItemIcon="lucide:plus"
  placeholder="Type to search or create..."
/>

<!-- 'always': shown whenever search is non-empty -->
<SelectMenu
  items={fruits}
  bind:value={alwaysValue}
  createItem="always"
  createItemLabel={(v) => `Add "${v}" as a new fruit`}
  createItemIcon="lucide:circle-plus"
  placeholder="Always offer create..."
/>

Persisting Created Items

Wire up onCreate to push the new value into your items array so it persists across opens. Even without doing this, the trigger will still render the created label because new values are tracked internally.

Selected: none · Items: 3

<script lang="ts">
  import { SelectMenu } from 'sv5ui';

  let value = $state('');
  let items = $state([
    { value: 'svelte', label: 'Svelte' },
    { value: 'react', label: 'React' },
    { value: 'vue', label: 'Vue' }
  ]);
</script>

<SelectMenu
  {items}
  bind:value
  createItem="lazy"
  createItemIcon="lucide:plus"
  placeholder="Add or pick a tag..."
  onCreate={(value) => {
    items = [...items, { value, label: value }];
  }}
/>

<p>Selected: {value || 'none'} · Items: {items.length}</p>

UI Slots

Use the ui prop to override classes on internal elements.

SlotDescription
rootRoot wrapper element
baseTrigger button element
leadingLeading section container
leadingIconLeading icon element
leadingAvatarLeading avatar element
trailingTrailing section container
trailingIconTrailing icon (chevron)
valueSelected value text
placeholderPlaceholder text
contentDropdown content container
inputSearch input element
viewportScrollable items viewport
emptyEmpty state container
groupLabelGroup label/heading
separatorVisual separator between groups
itemIndividual item container
itemIconIcon in item
itemAvatarAvatar in item
itemLabelItem label text
itemDescriptionItem description text
itemIndicatorSelected indicator (checkmark)

Snippets

Custom rendering for trigger and dropdown content.

SnippetDescription
leadingSlotCustom content before selected value on trigger
trailingSlotCustom content after selected value on trigger
selectedCustom rendering of selected value(s) in the trigger (multiple mode). Receives { items, remove, clear }
itemComplete item rendering (receives item, index, selected)
itemLeadingItem leading section (receives item, index, selected)
itemLabelItem label section (receives item, index, selected)
itemTrailingItem trailing/indicator section (receives item, index, selected)
emptyEmpty state (receives searchTerm)
contentComplete dropdown content (receives open, searchTerm)

Props

PropTypeDefault
itemsSelectMenuItemType[][]
valuestring | string[]-
multiplebooleanfalse
separatorstring', '
openbooleanfalse
placeholderstring-
searchPlaceholderstring'Search...'
variant'outline' | 'soft' | 'subtle' | 'ghost' | 'none''outline'
colorColorType'primary'
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
highlightbooleanfalse
loadingbooleanfalse
disabledbooleanfalse
iconstring-
leadingIconstring-
trailingIconstring'lucide:chevron-down'
selectedIconstring'lucide:check'
avatarAvatarProps-
filterFieldsstring[]['label', 'value']
ignoreFilterbooleanfalse
emptyTextstring'No results found.'
createItemboolean | 'always' | 'lazy'false
createItemLabelstring | ((value: string) => string)(value) => `Create "${value}"`
createItemIconstring | false-
onCreate(value: string) => void-
requiredbooleanfalse
namestring-
refHTMLElement | nullnull
style, title, role, tabindex, aria-*, data-*, event handlersHTMLAttributes-
classstring-
uiRecord<Slot, Class>-

Item Props

Each selectable item accepts these properties. Use type: 'label' for headings and type: 'separator' for dividers.

PropTypeDefault
valuestring-
labelstring-
descriptionstring-
iconstring-
avatarAvatarProps-
disabledbooleanfalse