Select
A single-choice menu styled like the text field.
No category
Source
<script lang="ts">
import 'portal-bits/styles.css';
import 'portal-bits/tokens.css';
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
{@render children()}
<script lang="ts">
import SelectExample from './SelectExample.svelte';
</script>
<SelectExample invalid={false} disabled={false} required={false} sizing={"stable"} />
<script lang="ts">
import { Select, type SelectOption } from 'portal-bits';
let {
invalid = false,
disabled = false,
required = false,
sizing = 'stable',
}: {
invalid?: boolean;
disabled?: boolean;
required?: boolean;
sizing?: 'stable' | 'dynamic';
} = $props();
const options: SelectOption[] = [
{ value: 'notes', label: 'Notes' },
{ value: 'drafts', label: 'Drafts' },
{ value: 'research', label: 'Shared research notes' },
{ value: 'archived', label: 'Archived', disabled: true },
];
let category = $state('');
</script>
<div class="field">
<Select
label="Record category"
bind:value={category}
placeholder="Choose a category"
{options}
{invalid}
{required}
{disabled}
{sizing}
aria-describedby={invalid ? 'category-error' : undefined}
/>
{#if invalid}<p id="category-error" class="error">Choose a category.</p>{/if}
<p aria-live="polite">{category || 'No category'}</p>
</div>
<style>
.field {
max-width: 340px;
}
.error {
color: var(--portal-error);
}
</style>
API reference
These tables list the props and snippets that Select declares. Attributes passed through to an element or primitive are listed under Forwarded attributes, not here.
Props
| Name | Type | Default | Required | Binding | Description |
|---|---|---|---|---|---|
label | string | None | Yes | One-way | The visible label, tied to the select with `for`. |
value | string | "" | No | Bindable | The selected option value. Empty means none selected. |
options | SelectOption[] | None | Yes | One-way | Choices as `{ value, label, disabled? }`. Values must be unique. |
placeholder | string | None | No | One-way | Adds an enabled empty option with this text, so the choice can return to empty. |
invalid | boolean | false | No | One-way | Sets `aria-invalid` on the trigger. Render the error text yourself. |
id | string | Generated id | No | One-way | The trigger id. A stable id is generated when you omit it. |
name | string | None | No | One-way | Submits the selected value under this name. |
required | boolean | false | No | One-way | Takes part in native form validation, even without `name`. |
disabled | boolean | false | No | One-way | Disables the trigger and every option. |
form | string | None | No | One-way | The id of the form that owns the control. |
theme | 'light' | 'dark' | light | No | One-way | Theme scope for the portaled content. Portaled content does not inherit the page scope. |
sizing | 'stable' | 'dynamic' | stable | No | One-way | Stable fills the field and never narrows below the longest option. Dynamic fits the selected label and eases between widths. |
class | string | "" | No | One-way | Extra classes, added after the component classes. |
Forwarded attributes
- Button attributes except `value` and `type`: `aria-*` and handlers. They land on the trigger. The trigger is `type="button"`.
The primitive underneath is a Bits UI peer dependency. Its own options are documented in the Bits UI Select documentation. Only the props listed on this page are part of the Select contract. No data attributes are documented as stable.
Limits
- Single choice only. Multiple selection and search are not supported.
- Reserve the empty value for the placeholder. An option with an empty value collides with it.
- The menu, trigger spacing, chevron, and dynamic width motion are adaptations. They reuse the input height, menu surface, type, and motion tokens.
- Menu options stay on one line. The menu grows past the trigger to fit the longest option, up to the viewport width.