Select

The menu opens with focus on the selected option, and arrow keys, Home, and End move between options. Use a Combobox when people need to search the list.

Priority
<script lang="ts">
    import * as Select from '@sivir-ui/svelte/components/select';

    const priorities = [
        { value: 'none', label: 'No priority' },
        { value: 'urgent', label: 'Urgent' },
        { value: 'high', label: 'High' },
        { value: 'medium', label: 'Medium' },
        { value: 'low', label: 'Low' }
    ];

    let selectedPriority = $state('medium');

    const selected = $derived(
        priorities.find((priority) => {
            return priority.value === selectedPriority;
        })
    );
</script>

<div class="flex flex-col gap-2">
    <span class="text-sm [font-weight:var(--font-weight-label,500)] text-foreground">Priority</span>
    <Select.Root bind:value={selectedPriority}>
        <Select.Trigger class="min-w-[220px]">
            {selected?.label ?? 'Select priority'}
        </Select.Trigger>
        <Select.Content>
            {#each priorities as priority (priority.value)}
                <Select.Item value={priority.value}>
                    {priority.label}
                </Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>

Installation

Copy the command below and run it in your terminal.

bunx @sivir-ui/svelte add select

Usage

Bind value on Select.Root; it is an empty string until an option is picked. An empty trigger shows the selected option's text. Add Select.Value inside it to set the placeholder.

import * as Select from '@sivir-ui/svelte/components/select';

let role = $state('');

<Select.Root bind:value={role}>
  <Select.Trigger>
    <Select.Value placeholder="Choose a role" />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value="designer">Designer</Select.Item>
    <Select.Item value="engineer">Engineer</Select.Item>
  </Select.Content>
</Select.Root>

Examples

Basic

Priority
<script lang="ts">
    import * as Select from '@sivir-ui/svelte/components/select';

    const priorities = [
        { value: 'none', label: 'No priority' },
        { value: 'urgent', label: 'Urgent' },
        { value: 'high', label: 'High' },
        { value: 'medium', label: 'Medium' },
        { value: 'low', label: 'Low' }
    ];

    let selectedPriority = $state('medium');

    const selected = $derived(
        priorities.find((priority) => {
            return priority.value === selectedPriority;
        })
    );
</script>

<div class="flex flex-col gap-2">
    <span class="text-sm [font-weight:var(--font-weight-label,500)] text-foreground">Priority</span>
    <Select.Root bind:value={selectedPriority}>
        <Select.Trigger class="min-w-[220px]">
            {selected?.label ?? 'Select priority'}
        </Select.Trigger>
        <Select.Content>
            {#each priorities as priority (priority.value)}
                <Select.Item value={priority.value}>
                    {priority.label}
                </Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>

Scrollable

Timezone
<script lang="ts">
    import * as Select from '@sivir-ui/svelte/components/select';

    const timezones = [
        { value: 'pacific-honolulu', label: 'Hawaii (HST)' },
        { value: 'america-anchorage', label: 'Alaska (AKST)' },
        { value: 'america-los-angeles', label: 'Pacific Time (PT)' },
        { value: 'america-denver', label: 'Mountain Time (MT)' },
        { value: 'america-chicago', label: 'Central Time (CT)' },
        { value: 'america-new-york', label: 'Eastern Time (ET)' },
        { value: 'america-halifax', label: 'Atlantic Time (AT)' },
        { value: 'america-sao-paulo', label: 'São Paulo (BRT)' },
        { value: 'atlantic-azores', label: 'Azores (AZOT)' },
        { value: 'europe-london', label: 'London (GMT)' },
        { value: 'europe-paris', label: 'Paris (CET)' },
        { value: 'europe-berlin', label: 'Berlin (CET)' },
        { value: 'europe-athens', label: 'Athens (EET)' },
        { value: 'europe-istanbul', label: 'Istanbul (TRT)' },
        { value: 'africa-cairo', label: 'Cairo (EET)' },
        { value: 'africa-johannesburg', label: 'Johannesburg (SAST)' },
        { value: 'europe-moscow', label: 'Moscow (MSK)' },
        { value: 'asia-dubai', label: 'Dubai (GST)' },
        { value: 'asia-karachi', label: 'Karachi (PKT)' },
        { value: 'asia-kolkata', label: 'Kolkata (IST)' },
        { value: 'asia-bangkok', label: 'Bangkok (ICT)' },
        { value: 'asia-shanghai', label: 'Shanghai (CST)' },
        { value: 'asia-tokyo', label: 'Tokyo (JST)' },
        { value: 'asia-seoul', label: 'Seoul (KST)' },
        { value: 'australia-sydney', label: 'Sydney (AEST)' },
        { value: 'pacific-auckland', label: 'Auckland (NZST)' }
    ];

    let selectedTimezone = $state('america-new-york');
    const selected = $derived(timezones.find((zone) => zone.value === selectedTimezone));
</script>

<div class="flex flex-col gap-2">
    <span class="text-sm [font-weight:var(--font-weight-label,500)] text-foreground">Timezone</span>
    <Select.Root value={selectedTimezone}>
        <Select.Trigger class="w-[240px]" variant="outline" size="md">
            {selected?.label ?? 'Select timezone'}
        </Select.Trigger>
        <Select.Content class="max-h-56">
            {#each timezones as timezone (timezone.value)}
                <Select.Item
                    value={timezone.value}
                    onclick={() => (selectedTimezone = timezone.value)}
                >
                    {timezone.label}
                </Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>

Add a max-h-* class to Select.Content to cap the menu height. Longer lists scroll inside it.

Dynamic width

Role
<script lang="ts">
    import * as Select from '@sivir-ui/svelte/components/select';

    const roles = [
        { value: 'viewer', label: 'Viewer' },
        { value: 'editor', label: 'Editor' },
        { value: 'billing', label: 'Billing administrator' }
    ];

    let role = $state('viewer');
    const selected = $derived(roles.find((r) => r.value === role));
</script>

<div class="flex flex-col gap-2">
    <span class="text-sm [font-weight:var(--font-weight-label,500)] text-foreground">Role</span>
    <Select.Root bind:value={role}>
        <Select.Trigger class="w-48" variant="outline">
            {selected?.label ?? 'Select role'}
        </Select.Trigger>
        <Select.Content dynamic>
            {#each roles as option (option.value)}
                <Select.Item value={option.value}>{option.label}</Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>

Set dynamic on Select.Content to size the menu to its longest option, even when the trigger is narrower.

Anatomy

Select.Root - Provides select state.

Select.Trigger - Opens the select menu.

Select.Value - Displays the selected value.

Select.Label - Labels a select section.

Select.Item - Defines a selectable option.

Select.Content - Renders select options.