Select
Selection & InputLocal guide ↗
import { Select } from "bits-ui" Grouped, single value
A bound string drives the trigger label while Group and GroupHeading give related options accessible structure
Submitted value: guided
<script lang="ts">
import { Select } from 'bits-ui';
import { Check, ChevronDown } from '@lucide/svelte';
const groups = [
{
label: 'Core support',
items: [
{ value: 'guided', label: 'Guided setup' },
{ value: 'priority', label: 'Priority support' },
{ value: 'managed', label: 'Managed rollout' }
]
},
{
label: 'Specialist help',
items: [
{ value: 'security', label: 'Security review' },
{ value: 'migration', label: 'Migration planning' },
{ value: 'training', label: 'Team training' }
]
}
];
const items = groups.flatMap((group) => group.items);
let value = $state('guided');
const selectedLabel = $derived(
items.find((item) => item.value === value)?.label ?? 'Choose a support plan'
);
</script>
<div class="grid w-full max-w-sm gap-3">
<label class="text-sm font-medium" for="support-plan-trigger">Support plan</label>
<Select.Root type="single" bind:value {items} name="supportPlan">
<Select.Trigger
id="support-plan-trigger"
class="input flex w-full items-center justify-between gap-3 text-left"
aria-label="Choose a support plan"
>
<span class="truncate">{selectedLabel}</span>
<ChevronDown class="size-4 shrink-0 text-ink-400" />
</Select.Trigger>
<Select.Portal>
<Select.Content
class="panel z-50 min-w-64 w-[var(--bits-select-anchor-width)] animate-pop-in p-1"
sideOffset={8}
>
<Select.Viewport class="max-h-64 p-1">
{#each groups as group (group.label)}
<Select.Group class="py-1">
<Select.GroupHeading
class="px-2 py-1.5 text-[0.68rem] font-semibold tracking-widest text-ink-400 uppercase"
>
{group.label}
</Select.GroupHeading>
{#each group.items as item (item.value)}
<Select.Item
value={item.value}
label={item.label}
class="flex cursor-default items-center gap-3 rounded-lg px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50 data-selected:text-brand-400"
>
{#snippet children({ selected })}
<span class="flex-1">{item.label}</span>
{#if selected}<Check class="size-4" />{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Group>
{/each}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<p class="muted text-xs">Submitted value: <span class="font-mono text-ink-200">{value}</span></p>
</div>
Multiple values
type="multiple" changes value to string[] and keeps the list open for successive choices
Alert channels 2 selected
Email digestSlack alerts
<script lang="ts">
import { Select } from 'bits-ui';
import { Check, ChevronDown } from '@lucide/svelte';
const items = [
{ value: 'email', label: 'Email digest' },
{ value: 'slack', label: 'Slack alerts' },
{ value: 'pager', label: 'Pager escalation' },
{ value: 'webhook', label: 'Webhook delivery' },
{ value: 'sms', label: 'SMS fallback' }
];
let value = $state<string[]>(['email', 'slack']);
const selectedLabels = $derived(
items.filter((item) => value.includes(item.value)).map((item) => item.label)
);
const triggerLabel = $derived(
selectedLabels.length > 0 ? selectedLabels.join(', ') : 'Choose alert channels'
);
</script>
<div class="grid w-full max-w-sm gap-3">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Alert channels</span>
<span class="muted text-xs">{value.length} selected</span>
</div>
<Select.Root type="multiple" bind:value {items} name="alertChannels">
<Select.Trigger
class="input flex w-full items-center justify-between gap-3 text-left"
aria-label="Choose alert channels"
>
<span class="truncate">{triggerLabel}</span>
<ChevronDown class="size-4 shrink-0 text-ink-400" />
</Select.Trigger>
<Select.Portal>
<Select.Content
class="panel z-50 min-w-64 w-[var(--bits-select-anchor-width)] animate-pop-in p-1"
sideOffset={8}
>
<Select.Viewport class="p-1">
{#each items as item (item.value)}
<Select.Item
value={item.value}
label={item.label}
class="flex cursor-default items-center gap-3 rounded-lg px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50 data-selected:text-brand-400"
>
{#snippet children({ selected })}
<span class="flex-1">{item.label}</span>
<span
class="grid size-5 place-items-center rounded border border-ink-700 data-[selected=true]:border-brand-500"
data-selected={selected}
>
{#if selected}<Check class="size-3.5" />{/if}
</span>
{/snippet}
</Select.Item>
{/each}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<div class="flex flex-wrap gap-2">
{#each selectedLabels as label (label)}
<span class="chip">{label}</span>
{/each}
</div>
</div>
type is required and discriminates the value shape: "single" binds a string, while "multiple" binds a string array. Supplying items gives Bits UI the value/label pairs it needs for closed-trigger typeahead and single-select form autofill.
Set name to render the hidden form input. A required select must provide both required and name. Content is portalled to document.body by default, and exposes anchor width and available-height CSS variables for collision-safe sizing.