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.
| Slot | Description |
|---|---|
root | Root wrapper element |
base | Trigger button element |
leading | Leading section container |
leadingIcon | Leading icon element |
leadingAvatar | Leading avatar element |
trailing | Trailing section container |
trailingIcon | Trailing icon (chevron) |
value | Selected value text |
placeholder | Placeholder text |
content | Dropdown content container |
input | Search input element |
viewport | Scrollable items viewport |
empty | Empty state container |
groupLabel | Group label/heading |
separator | Visual separator between groups |
item | Individual item container |
itemIcon | Icon in item |
itemAvatar | Avatar in item |
itemLabel | Item label text |
itemDescription | Item description text |
itemIndicator | Selected indicator (checkmark) |
Snippets
Custom rendering for trigger and dropdown content.
| Snippet | Description |
|---|---|
leadingSlot | Custom content before selected value on trigger |
trailingSlot | Custom content after selected value on trigger |
selected | Custom rendering of selected value(s) in the trigger (multiple mode). Receives { items, remove, clear } |
item | Complete item rendering (receives item, index, selected) |
itemLeading | Item leading section (receives item, index, selected) |
itemLabel | Item label section (receives item, index, selected) |
itemTrailing | Item trailing/indicator section (receives item, index, selected) |
empty | Empty state (receives searchTerm) |
content | Complete dropdown content (receives open, searchTerm) |
Props
| Prop | Type | Default |
|---|---|---|
items | SelectMenuItemType[] | [] |
value | string | string[] | - |
multiple | boolean | false |
separator | string | ', ' |
open | boolean | false |
placeholder | string | - |
searchPlaceholder | string | 'Search...' |
variant | 'outline' | 'soft' | 'subtle' | 'ghost' | 'none' | 'outline' |
color | ColorType | 'primary' |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
highlight | boolean | false |
loading | boolean | false |
disabled | boolean | false |
icon | string | - |
leadingIcon | string | - |
trailingIcon | string | 'lucide:chevron-down' |
selectedIcon | string | 'lucide:check' |
avatar | AvatarProps | - |
filterFields | string[] | ['label', 'value'] |
ignoreFilter | boolean | false |
emptyText | string | 'No results found.' |
createItem | boolean | 'always' | 'lazy' | false |
createItemLabel | string | ((value: string) => string) | (value) => `Create "${value}"` |
createItemIcon | string | false | - |
onCreate | (value: string) => void | - |
required | boolean | false |
name | string | - |
ref | HTMLElement | null | null |
style, title, role, tabindex, aria-*, data-*, event handlers | HTMLAttributes | - |
class | string | - |
ui | Record<Slot, Class> | - |
Item Props
Each selectable item accepts these properties. Use type: 'label' for headings and type: 'separator' for dividers.
| Prop | Type | Default |
|---|---|---|
value | string | - |
label | string | - |
description | string | - |
icon | string | - |
avatar | AvatarProps | - |
disabled | boolean | false |