# Select

Listbox-based single or multiple select with bindable value and Trigger/Value/Content/Item/Label subparts.

- Package: `@mielui/svelte`
- Component version: `2.3.0`
- Depends on Mielui components: _internal/utils, popover, button
- Shared utilities: components/_internal/button-attributes, components/_internal/surface, hugeicons-icon, utils.cn, utils.createContext, utils.dynamicWidth, utils.travelingHighlight

## Install

~~~~sh
pnpm dlx @mielui/svelte add select
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |
| dynamic | boolean \| undefined | false | No | No | Sizes the list to its longest option, even when the trigger is narrower. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> | - | Yes | No | Content rendered inside. |



### Item

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| value | string \| (string & string[]) | - | Yes | No | Value this option selects. |
| label | string \| undefined | - | No | No | Text shown in the trigger when it differs from the option's content. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Label

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> | - | Yes | No | Content rendered inside. |



### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| open | boolean \| undefined | false | No | Yes | Whether it is open. Bind it to control the state from outside. |
| onOpenChange | ((open: boolean) => void) \| undefined | - | No | No | Called with the new state whenever it opens or closes. |
| name | string \| undefined | - | No | No | Field name submitted with the form. |
| disabled | boolean \| undefined | false | No | No | Prevents interaction and dims the control. |
| required | boolean \| undefined | false | No | No | Requires a value before the form can be submitted. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| type | "single" \| "multiple" \| undefined | 'single' | No | No | `single` keeps one value. `multiple` allows several and makes `value` an array. |
| value | string \| string[] \| undefined | type === 'multiple' ? [] : '' | No | Yes | Selected value, or an array in multiple mode. Bindable. |
| onValueChange | ((value: string) => void) \| ((value: string[]) => void) \| undefined | - | No | No | Called with the new value when it changes. |



### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. Defaults to `Select.Value`. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| variant | ButtonVariant \| undefined | 'outline' | No | No | Button style of the trigger. |
| aria-labelledby | string \| undefined | - | No | No | Id of the element that labels the trigger. |
| aria-invalid | Booleanish \| "grammar" \| "spelling" \| null \| undefined | - | No | No | Marks the trigger as invalid for assistive technology and error styling. |
| style | string \| undefined | - | No | No | Inline styles for the element. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when it is clicked or activated. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height of the trigger. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the DOM element. |
| unstyled | boolean \| undefined | - | No | No | Forwarded to Button: render with `class` alone, skipping variant/size. |
| onopen | (() => void) \| undefined | - | No | No | Called immediately before this trigger opens its popover. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Value

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| placeholder | string \| undefined | 'Select' | No | No | Text shown while nothing is selected. |
| class | string \| undefined | - | No | No | Classes added to the element. |



### Public types

~~~~ts
import type { ButtonVariant } from '@mielui/svelte/components/button';
import type { PopoverTriggerProps } from '@mielui/svelte/components/popover';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
import type { MenuRowButtonProps } from '../_internal/button-attributes';
import Root from './select.svelte';
import Content from './select-content.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import Trigger from './select-trigger.svelte';
import Value from './select-value.svelte';

export type SelectState<Value extends string | string[] = string> = {
    value: Value;
    selectedLabel: string;
};

type SelectRootProps = {
    /** Whether it is open. Bind it to control the state from outside. */
    open?: boolean;
    /** Called with the new state whenever it opens or closes. */
    onOpenChange?: (open: boolean) => void;
    /** Field name submitted with the form. */
    name?: string;
    /** Prevents interaction and dims the control. */
    disabled?: boolean;
    /** Requires a value before the form can be submitted. */
    required?: boolean;
    /** Content rendered inside. */
    children?: Snippet;
};

export type SelectProps = SelectRootProps &
    (
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type?: 'single';
              /** Selected value, or an array in multiple mode. Bindable. */
              value?: string;
              /** Called with the new value when it changes. */
              onValueChange?: (value: string) => void;
          }
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type: 'multiple';
              /** Selected value, or an array in multiple mode. Bindable. */
              value?: string[];
              /** Called with the new value when it changes. */
              onValueChange?: (value: string[]) => void;
          }
    );

export type SelectTriggerProps = {
    /** Content rendered inside. Defaults to `Select.Value`. */
    children?: Snippet;
    /** Classes added to the element. */
    class?: string;
    /** Button style of the trigger. */
    variant?: ButtonVariant;
    /** Id of the element that labels the trigger. */
    'aria-labelledby'?: string;
    /** Marks the trigger as invalid for assistive technology and error styling. */
    'aria-invalid'?: HTMLButtonAttributes['aria-invalid'];
} & Omit<PopoverTriggerProps, 'children' | 'class' | 'variant' | 'icon'>;

export type SelectContentProps = {
    /** Surface treatment. Omit it to inherit `--mielui-surface` from the theme. */
    surface?: 'solid' | 'glass';
    /** Sizes the list to its longest option, even when the trigger is narrower. */
    dynamic?: boolean;
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children: Snippet;
};

export type SelectLabelProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children: Snippet;
};

export type SelectItemProps = {
    /** Value this option selects. */
    value: string;
    /** Text shown in the trigger when it differs from the option's content. */
    label?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & MenuRowButtonProps;

export type SelectValueProps = {
    /** Text shown while nothing is selected. */
    placeholder?: string;
    /** Classes added to the element. */
    class?: string;
};

export { Content, Item, Label, Root, Trigger, Value };
~~~~

## Examples

### Controlled

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Select from '@mielui/svelte/components/select';

    const regions = ['Europe', 'North America', 'Asia Pacific'];

    let open = $state(false);
    let region = $state('Europe');
    let lastChange = $state('none yet');
</script>

<div class="flex flex-col items-center gap-3">
    <div class="flex items-end gap-2">
        <div class="grid gap-1.5">
            <span id="region-label" class="text-sm font-medium">Region</span>
            <Select.Root
                bind:value={region}
                bind:open
                onOpenChange={(next) => {
                    lastChange = next ? 'opened' : 'closed';
                }}
            >
                <Select.Trigger aria-labelledby="region-label" class="w-48" />
                <Select.Content>
                    {#each regions as option (option)}
                        <Select.Item value={option}>{option}</Select.Item>
                    {/each}
                </Select.Content>
            </Select.Root>
        </div>
        <Button
            variant="ghost"
            onclick={() => {
                open = true;
            }}
        >
            Open the list
        </Button>
    </div>
    <p class="text-sm text-foreground-muted">Last change by a person: {lastChange}</p>
</div>
~~~~

### Dynamic Width

~~~~svelte
<script lang="ts">
    import * as Select from '@mielui/svelte/components/select';

    const roles = ['Viewer', 'Editor', 'Billing administrator'];

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

<div class="flex flex-col gap-2">
    <span id="dynamic-width-label" class="text-sm font-medium">Role</span>
    <Select.Root bind:value={role}>
        <Select.Trigger aria-labelledby="dynamic-width-label" class="w-40" />
        <Select.Content dynamic>
            {#each roles as option (option)}
                <Select.Item value={option}>{option}</Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>
~~~~

### Glass

~~~~svelte
<script lang="ts">
    import * as Select from '@mielui/svelte/components/select';

    const priorities = ['No priority', 'Urgent', 'High', 'Medium', 'Low'];

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

<div class="flex flex-col gap-2">
    <span id="glass-priority-label" class="text-sm font-medium">Priority</span>
    <Select.Root bind:value={priority}>
        <Select.Trigger aria-labelledby="glass-priority-label" class="min-w-56" />
        <Select.Content surface="glass">
            {#each priorities as option (option)}
                <Select.Item value={option}>{option}</Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>
~~~~

### Multiple

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Select from '@mielui/svelte/components/select';

    const options = [
        {
            value: 'design',
            label: 'Design'
        },
        {
            value: 'engineering',
            label: 'Engineering'
        },
        {
            value: 'support',
            label: 'Support'
        },
        {
            value: 'finance',
            label: 'Finance',
            disabled: true
        }
    ];

    let teams = $state<string[]>(['design']);
    let submitted = $state('');

    function clear() {
        teams = [];
        submitted = '';
    }

    function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
        event.preventDefault();
        const values = new FormData(event.currentTarget).getAll('teams').filter((value) => {
            return typeof value === 'string' && value.length > 0;
        });
        submitted = values.length ? `Saved: ${values.join(', ')}` : 'No teams selected.';
    }
</script>

<form class="flex w-full max-w-sm flex-col gap-4" onsubmit={submit}>
    <Select.Root type="multiple" bind:value={teams} name="teams">
        <Select.Trigger aria-label="Teams" class="w-full">
            <Select.Value placeholder="Select teams" />
        </Select.Trigger>
        <Select.Content>
            {#each options as option (option.value)}
                <Select.Item value={option.value} disabled={option.disabled}>
                    {option.label}
                </Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
    <div class="flex items-center gap-2">
        <Button type="submit">Save teams</Button>
        <Button variant="outline" onclick={clear}>Clear</Button>
    </div>
    <p role="status" class="min-h-5 text-sm text-foreground-muted">
        {submitted || `${teams.length} selected`}
    </p>
</form>
~~~~

### Scrollable

~~~~svelte
<script lang="ts">
    import * as Select from '@mielui/svelte/components/select';

    const timezones = [
        'Hawaii (HST)',
        'Alaska (AKST)',
        'Pacific Time (PT)',
        'Mountain Time (MT)',
        'Central Time (CT)',
        'Eastern Time (ET)',
        'Atlantic Time (AT)',
        'São Paulo (BRT)',
        'Azores (AZOT)',
        'London (GMT)',
        'Paris (CET)',
        'Berlin (CET)',
        'Athens (EET)',
        'Istanbul (TRT)',
        'Cairo (EET)',
        'Johannesburg (SAST)',
        'Moscow (MSK)',
        'Dubai (GST)',
        'Karachi (PKT)',
        'Kolkata (IST)',
        'Bangkok (ICT)',
        'Shanghai (CST)',
        'Tokyo (JST)',
        'Seoul (KST)',
        'Sydney (AEST)',
        'Auckland (NZST)'
    ];

    let timezone = $state('Eastern Time (ET)');
</script>

<div class="flex flex-col gap-2">
    <span id="timezone-label" class="text-sm font-medium">Time zone</span>
    <Select.Root bind:value={timezone}>
        <Select.Trigger aria-labelledby="timezone-label" class="min-w-60" />
        <Select.Content class="max-h-56">
            {#each timezones as option (option)}
                <Select.Item value={option}>{option}</Select.Item>
            {/each}
        </Select.Content>
    </Select.Root>
</div>
~~~~

For the rendered reference, visit [/docs/components/select](/docs/components/select).
