Components Forms
Select
StableChoose one value from a list, with typeahead, groups, disabled options and form submission.
Use Select when the user picks exactly one value from a known list of roughly 5 to 30 options. For long or searchable lists use Combobox; for several values use MultiSelect; for 2 to 4 visible choices consider RadioGroup. Select is a select-only combobox (WAI-ARIA APG): focus stays on the trigger and the active option is announced through aria-activedescendant. It integrates with FormField and submits through a hidden input.
Overview
Controls what this member can change.
import { FormField, Select } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';
const roles: SelectOptions = [
{ value: 'owner', label: 'Owner', description: 'Full access, including billing' },
{ value: 'admin', label: 'Admin', description: 'Manage users, templates and integrations' },
{ value: 'editor', label: 'Editor', description: 'Create and edit templates' },
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
];
export function Basic() {
return (
<div style={{ width: 320 }}>
<FormField label="Role" description="Controls what this member can change.">
<Select options={roles} placeholder="Choose a role" name="role" />
</FormField>
</div>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Controls what this member can change.
Controls what this member can change.
Controls what this member can change.
Import
import { Select, type SelectOptions } from '@bindshq/stance-react';Platforms
Select has native counterparts. They share Stance tokens and intent, and use each platform’s own controls and conventions. The live previews on this page are the web implementation.
import { Select, type SelectOptions } from '@bindshq/stance-react';Examples
import { FormField, Select } from '@bindshq/stance-react';
export function Groups() {
return (
<div style={{ width: 320 }}>
<FormField label="Default timezone">
<Select
defaultValue="europe-london"
options={[
{
label: 'Europe',
options: [
{ value: 'europe-london', label: 'London (GMT)' },
{ value: 'europe-paris', label: 'Paris (CET)' },
{ value: 'europe-berlin', label: 'Berlin (CET)' },
],
},
{
label: 'Asia',
options: [
{ value: 'asia-kolkata', label: 'Kolkata (IST)' },
{ value: 'asia-singapore', label: 'Singapore (SGT)' },
{ value: 'asia-dubai', label: 'Dubai (GST)' },
],
},
]}
/>
</FormField>
</div>
);
}import { FormField, Icon, Select } from '@bindshq/stance-react';
export function WithIcons() {
return (
<div style={{ width: 320 }}>
<FormField label="Deploy through">
<Select
defaultValue="exchange"
options={[
{ value: 'exchange', label: 'Exchange Online', icon: <Icon name="mail" /> },
{ value: 'relay', label: 'Outlook relay', icon: <Icon name="send" /> },
{
value: 'gmail',
label: 'Google Workspace (coming soon)',
icon: <Icon name="globe" />,
disabled: true,
},
]}
/>
</FormField>
</div>
);
}import { Select } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';
const roles: SelectOptions = [
{ value: 'owner', label: 'Owner', description: 'Full access, including billing' },
{ value: 'admin', label: 'Admin', description: 'Manage users, templates and integrations' },
{ value: 'editor', label: 'Editor', description: 'Create and edit templates' },
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
];
export function Sizes() {
return (
<div style={{ display: 'grid', gap: 12, width: 280 }}>
<Select aria-label="Small" size="sm" options={roles} defaultValue="editor" />
<Select aria-label="Medium" size="md" options={roles} defaultValue="editor" />
<Select aria-label="Large" size="lg" options={roles} defaultValue="editor" />
</div>
);
}import * as React from 'react';
import { FormField, Select } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';
const roles: SelectOptions = [
{ value: 'owner', label: 'Owner', description: 'Full access, including billing' },
{ value: 'admin', label: 'Admin', description: 'Manage users, templates and integrations' },
{ value: 'editor', label: 'Editor', description: 'Create and edit templates' },
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
];
export function Controlled() {
const [role, setRole] = React.useState<string | null>('viewer');
return (
<div style={{ display: 'grid', gap: 8, width: 320 }}>
<FormField label="Role">
<Select options={roles} value={role} onValueChange={setRole} />
</FormField>
<span style={{ color: 'var(--st-color-fg-muted)', fontSize: 'var(--st-font-size-sm)' }}>
Selected: {role ?? 'none'}
</span>
</div>
);
}Choose a department to continue
import { FormField, Select } from '@bindshq/stance-react';
export function States() {
return (
<div style={{ display: 'grid', gap: 16, width: 320 }}>
<FormField label="Department" error="Choose a department to continue" required>
<Select
options={[
{ value: 'legal', label: 'Legal' },
{ value: 'sales', label: 'Sales' },
]}
placeholder="Choose a department"
/>
</FormField>
<FormField label="Plan" disabled>
<Select options={[{ value: 'business', label: 'Business' }]} defaultValue="business" />
</FormField>
</div>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
options* required | SelectOptions | None | Options `{ value, label, description?, icon?, disabled? }`, or groups `{ label, options }`. |
value | string | null | None | Selected value (controlled). |
defaultValue | string | null | null | Initial value when uncontrolled. |
onValueChange | (value: string) => void | None | Called when the user picks an option. |
placeholder | string | 'Select…' | Shown when nothing is selected. |
name | string | None | Form field name; a hidden input carries the value. |
form | string | None | Associate with a form by id. |
size | 'sm' | 'md' | 'lg' | 'md' | Control height: 32, 40 or 48px. |
required / disabled / invalid | boolean | None | Also inherited from FormField. |
open / defaultOpen / onOpenChange | boolean / function | None | Popup open state. |
renderOption | (option, { selected, active }) => ReactNode | None | Custom option content. |
emptyText | ReactNode | 'No options' | Shown when options is empty. |
contentClassName | string | None | Class for the popup. |
...props | ButtonHTMLAttributes | None | Native attributes for the trigger, e.g. aria-label when used without FormField. |
Accessibility
- The trigger is a button with role="combobox", aria-haspopup="listbox", aria-expanded and aria-controls.
- Inside FormField it is labelled by the visible label and described by the description and error.
- Options use role="option" with aria-selected; groups use role="group" labelled by their heading.
- Focus never leaves the trigger; the active option is conveyed through aria-activedescendant.
- Disabled options expose aria-disabled and are skipped by arrow keys and typeahead.
Keyboard
| Keys | Action |
|---|---|
| Enter Space ArrowDown ArrowUp | Open the list at the selected option. |
| Home End | Open at, or move to, the first or last option. |
| ArrowDown ArrowUp | Move the active option. |
| PageDown PageUp | Move by ten options. |
| A to Z | Typeahead by label, whether open or closed. |
| Enter Space | Select the active option and close. |
| Tab | Select the active option and move on. |
| Escape | Close without changing the value. |
Usage guidelines
Do
- Order options logically: alphabetically, by frequency, or by magnitude.
- Use a placeholder that tells people what to choose.
Don’t
- Use Select for fewer than three options; use RadioGroup or a Switch.
- Use Select for long lists people need to search; use Combobox.