Select
Form-focused select dropdown with label, validation, keyboard navigation, grouped options, and form integration.
Playground
<script lang="ts">
import { Select } from '@urbicon-ui/blocks';
const options = [
{ label: 'Svelte', value: 'svelte' },
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' }
];
</script>
<Select
{options}
clearable
label="Framework"
placeholder="Choose a framework"
/>01 Examples
Grouped options
<Select label="Timezone" groups={timezoneGroups} placeholder="Select timezone" />Per-option disabled
<Select
label="Plan"
options={[
{ label: 'Free', value: 'free' },
{ label: 'Starter', value: 'starter' },
{ label: 'Pro', value: 'pro' },
{ label: 'Enterprise (Contact us)', value: 'enterprise', disabled: true }
]}
placeholder="Choose a plan"
/>Helper & error
error overrides helper when both are set.<Select
label="Language"
options={countries}
helper="Choose your preferred language"
placeholder="Select..."
/>
<Select
label="Department"
options={countries}
error="Please select a department"
placeholder="Select..."
/>Form Integration
Form value: editor
<div class="border-border-subtle bg-surface-elevated w-full space-y-4 rounded-2xl border p-5">
<Select
label="User Role"
name="role"
bind:value={selectedRole}
options={[
{ label: 'Viewer', value: 'viewer' },
{ label: 'Editor', value: 'editor' },
{ label: 'Admin', value: 'admin' }
]}
/>
<p class="text-text-tertiary text-xs">
Form value: <code class="text-text-primary">{selectedRole ?? 'null'}</code>
</p>
</div>02 Customization
Pill Trigger via slotClasses
<Select
label="Sort by"
options={[
{ label: 'Newest first', value: 'newest' },
{ label: 'Price ascending', value: 'price-asc' },
{ label: 'Price descending', value: 'price-desc' }
]}
value="newest"
slotClasses={{
trigger: 'rounded-full',
listbox: 'rounded-xl shadow-[var(--blocks-shadow-lg)]',
option: 'rounded-lg'
}}
/>unstyled strips every slot's default classes while the
combobox ARIA wiring, keyboard navigation, and hidden form input keep working — rebuild both
surfaces through slotClasses. A field treatment your
forms share (with Input and Combobox) belongs in BlocksProvider presets (presets.Select) rather than per-instance overrides — see Customization.
03 Accessibility
ARIA Combobox
The trigger uses role="combobox" with aria-expanded, aria-haspopup="listbox", and aria-controls. Options use role="option" with aria-selected. Label, error, and helper text are
linked via aria-labelledby and aria-describedby.
Keyboard
Enter / Space / Arrow Down opens the dropdown. Arrow Up/Down navigates options. Home/End jump to first/last. Escape closes and returns focus.
Form Submission
When the name prop is set, a hidden <input> element carries the selected value for native
form submission without JavaScript.
04 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
class | string | — | Extra classes merged onto the root wrapper element. | |
clearable | boolean | false | Show a clear button when a value is selected. | |
closeOnClickOutside | boolean | — | Whether the listbox closes on outside click. Default true.
Set to false to pin the listbox open while the consumer manages
dismissal explicitly. | |
closeOnEscape | boolean | — | Whether the listbox closes on Escape key. Default true.
Set to false for inline contexts where Escape should be intercepted
by an outer widget (e.g. a row editor that wants to revert on Escape). | |
closeOnSelect | boolean | — | Whether the listbox closes after a selection is made.
Defaults to true in single-select and false in multi-select — the
multi-select default lets users tick multiple options without re-opening,
matching the established multi-select pattern. | |
customItem | Snippet<[SelectOption<T>, boolean, () => void]> | — | Replace the default per-option rendering. Receives the option, its
selection state, and a toggle callback (call to select/deselect).
The outer <div role="option"> container is still owned by Select for
ARIA correctness — the snippet renders the option's visible contents.
Positional args: (option, isSelected, toggle). | |
customTrigger | Snippet<[SelectOption<T>[], boolean, () => void]> | — | Replace the entire trigger button (chevron, label, clear control) with a
custom element. Receives the selected options, current open state, and a
clear callback. Use this for icon-only triggers, badge-counter triggers,
or any non-standard chrome — the Select still owns open/close + selection
state via the returned callbacks.
Positional args: (selected, open, clear). | |
customTriggerContent | Snippet<[SelectOption<T>[]]> | — | Replace only the trigger's text/label area (chevron and clear button stay
intact). Receives the selected options. Useful for showing icons next to
the label, badges with counts, or formatted multi-select summaries while
keeping the standard outlined-Select chrome.
Positional args: (selected). | |
disabled | boolean | false | Whether the Select is disabled and non-interactive | |
error | string | — | Error message below the select. Overrides helper and forces danger styling. | |
groups | SelectGroup<T>[] | — | Grouped options with section labels. Takes precedence over options. | |
helper | string | — | Helper text below the select. Hidden when error is set. | |
id | string | — | Explicit id for the component. Auto-generated if omitted.
The id lands on the component's wrapper, which is not a labelable element.
The focusable trigger is derived from it as ${id}-trigger — that is
the id an external <label for> has to address, since a for pointing at
the wrapper focuses nothing. | |
label | string | — | Label text displayed above the select, auto-linked via id. | |
messageType variant | errorhelper | helper | Controls the messageType behavior and appearance of the Select component. Available options: error, helper. | |
mint | MintProp | 'none' | Micro-interaction preset applied to the trigger — the default trigger
button, or the wrapper around a customTrigger. Only applies while
not disabled. | |
multiPlaceholder | string | ((selected: SelectOption<T>[]) => string) | — | Placeholder shown when one or more values are picked. Pass a string for a static label ("3 selected") or a function that receives the currently selected options and returns a custom string. When unset, the trigger lists the selected labels comma-separated. | |
multiple | falsetrue | — | Single-select mode (the default). Omit or set to false explicitly. | |
name | string | — | Shared name for a hidden input for native form submission. | |
nullOption | string | NullOptionConfig | — | Render an explicit "no value" option as the first row of the listbox.
Selecting it sets the bound value to null. When value is null,
the trigger displays this label instead of the placeholder.
Pass a string to use it as the label, or a NullOptionConfig for more control.
Ignored when groups is set — group structures own their option list. | |
onClickOutside | () => void | — | Fires after an outside click closes the listbox. Use for analytics
or side-effects on dismiss. Does NOT control whether the listbox
closes — that is governed by closeOnClickOutside. | |
onEscape | () => void | — | Fires after Escape closes the listbox. Use for analytics or to clear
ephemeral state on dismiss. Does NOT control whether the listbox
closes — that is governed by closeOnEscape. | |
onOpenChange | (open: boolean) => void | — | Fires when the listbox opens or closes from user interaction (trigger
click, keyboard, selection, Escape, Tab-out, outside click). Receives the
new open state — use it e.g. to lazy-load options on first open. Not
called when the consumer writes bind:open directly. | |
onValueChange | (value: T | null) => void | — | Fires after the selected value changes. Receives the new value or null on clear. | |
open | boolean | false | Controls whether the listbox is open. Supports bind:open for parents that
need to coordinate open/close (e.g. attaching a custom trigger button outside
the Select wrapper). | |
options | SelectOption<T>[] | — | Flat list of selectable options. | |
placeholder | string | 'Select...' | Text shown when no value is selected. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Select: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
required | boolean | false | Adds a required asterisk to the label. | |
selected variant | true | false | Controls the selected behavior and appearance of the Select component. Available options: true. | |
selectionIndicator | checkmarknone | — | Selection indicator rendered next to each option.
- 'checkmark' — trailing check icon (default)
- 'none' — no indicator (typical when using customItem) | |
size variant | lgmdsmxl +1 more | md | Controls the dimensions, padding, and text size of the Select. Affects the component's physical footprint. Available options: lg, md, sm, and 2 more. | |
slotClasses | Partial<Record<SelectSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: wrapper (root —
what class also targets) | base | trigger | triggerText | placeholder |
chevron | clear | listbox | option | optionLabel | optionCheck |
optionCheckbox | group | groupLabel | label | message. | |
syncWidth | boolean | true | Whether the listbox matches the trigger's width. Set false for icon-only
or compact triggers where the listbox should size to its content instead. | |
tier variant | commitmodify | modify | Selects the semantic radius tier of the Select — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. | |
unstyled | boolean | — | Remove all default tv() classes. | |
usePortal | boolean | true | When true, the listbox is rendered into the browser top layer via the native
popover API, so it cannot be clipped by overflow: auto ancestors. When
false, the listbox stays in the regular DOM flow — useful inside other
popovers/portals to avoid double-portaling. | |
value | T[] | [] | Currently selected values. Supports bind:value. | |
variant | SelectVariants['variant'] | 'outlined' | Visual style.
- outlined (default) — visible border, surface-base background
- filled — surface-interactive fill, no border (compact toolbars)
- ghost — transparent until hover/focus (dense menus, inline editors)
- underline — bottom-line only, no border-box (editorial knob-strips,
docs playgrounds) |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
SelectValue | type | helper | 0 | Primitive value types accepted by Select / Combobox options. Strings are the default; numbers and booleans cover form fields bound to numeric IDs or yes/no flags without forcing the consumer to convert back and forth at every call site. | |
SelectOption | interface | helper | 1 | A single select option. | |
SelectGroup | interface | helper | 1 | A labelled group of options. | |
NullOptionConfig | interface | helper | 0 | Configuration for an explicit "no value" option rendered at the top of the listbox.
Selecting it sets value to null and fires onValueChange(null).
Pass a string to use it as the label, or an object for additional control:
nullOption="No selection"
nullOption={{ label: 'Leave unassigned' }} | |
SelectSingleProps | interface | props | 0 | Single-select arm. value is T | null, selectionIndicator excludes
'checkbox' (a checkmark or no indicator), nullOption is available,
multiPlaceholder is *not* — there's never more than one selected label
to summarize. | |
SelectMultipleProps | interface | props | 0 | Multi-select arm. value is T[], selectionIndicator excludes
'checkmark' (a checkbox or no indicator), multiPlaceholder is
available, nullOption is *not* — clearing in multi mode empties the
array instead of binding a null sentinel. | |
SelectProps | type | helper | 0 | Discriminated union over multiple. Consumers pick a single shape at the
call site and the value type narrows accordingly — <Select multiple bind:value>
binds to T[], <Select bind:value> binds to T | null. See
SelectSingleProps / SelectMultipleProps for the per-mode details. | |
SelectVariants | type | variant | 0 | — | |
SelectSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
06 Installation
Import
import { Select } from '@urbicon-ui/blocks';