Single-choice picker for finite option lists.
- Use
options(list)for local options - Use
optionsFrom(fetcher, config)for remote datasets - Add
searchable()when a richer lookup experience is needed - Use
defaultSelected(...)/selected(...)when the form should open on a meaningful choice instead of an empty placeholder
Select.web.tsxtsx
| 1 | const schema = { |
| 2 | workspace: field.select('Workspace') |
| 3 | .options([ |
| 4 | { label: 'Operations workspace', value: 'ops' }, |
| 5 | { label: 'Revenue cockpit', value: 'revenue' }, |
| 6 | { label: 'Support command center', value: 'support' }, |
| 7 | ]) |
| 8 | .defaultSelected('revenue') |
| 9 | .required(), |
| 10 | } |
optionsFrom config
keycacheTtl(default60000)debounce(default300)minChars(default0)dependsOn(default[])initialOptions(default[])fetchOnMount(defaulttrue)keepPreviousOptions(defaulttrue)preserveOnError(defaulttrue)
Render a picker
Select fields expose their resolved options through fieldController(name). Use them in a native <select>, a design-system combobox, a modal, or a bottom sheet.
InlineExample-1.tsxtsx
| 1 | const schema = { |
| 2 | city: field |
| 3 | .select('City') |
| 4 | .optionsFrom(fetchCities, { |
| 5 | key: 'city-search', |
| 6 | debounce: 250, |
| 7 | minChars: 2, |
| 8 | }) |
| 9 | .searchable(), |
| 10 | } |
| 11 | |
| 12 | const form = useFormBridge(schema) |
| 13 | const city = form.fieldController('city') |
| 14 | |
| 15 | <select |
| 16 | value={String(city.value ?? '')} |
| 17 | disabled={city.disabled} |
| 18 | onChange={(event) => city.onChange(event.target.value)} |
| 19 | onBlur={city.onBlur} |
| 20 | > |
| 21 | {city.options?.map((option) => ( |
| 22 | <option key={String(option.value)} value={option.value}> |
| 23 | {option.label} |
| 24 | </option> |
| 25 | ))} |
| 26 | </select> |
For remote search, use useAsyncOptions() to own query/loading/error state and pass its options to your picker UI.
Defaults, inheritance & field methods
- defaultValue is
'' - type is
select - Selected values are stored as the option
value, so bothstringandnumberare supported - Shared methods: see Base field builder
- String-specific helpers are not available on select fields
- Picker UIs consume
form.fieldController(name)and remain entirely application-owned
Select-specific methods:
| Method | Type | Description |
|---|---|---|
options(list) | string[] | { label, value }[] | Loads local options from plain strings or explicit label/value objects. |
optionsFrom(fetcher, config) | fetcher + async config | Loads remote options with debounce, cache, dependency tracking, and loading state support. |
searchable(value = true) | value?: boolean | Enables search-oriented picker behavior. |
defaultSelected(valueOrOption) | value | option object | Pre-selects an option by raw value or option object. |
selected(valueOrOption) | value | option object | Alias for defaultSelected() with more explicit wording. |
oneOf(values, message?) | Array<value | option> | Additional allow-list check on top of options(). Useful when the option list is broader than what the current form should actually accept (e.g. gating promo tiers by user role). |
notOneOf(values, message?) | Array<value | option> | Deny-list version of oneOf() - rejects a subset of values even if they exist in options(). |
disallowPlaceholder(message?) | message?: string | Shorthand for required() with a select-oriented default message ("Please select an option."). Use it when the placeholder row is visible but must not be submitted. |
Recipes
Patterns that showcase select-specific strengths.
Preselected workspace
Workspace.tsxtsx
| 1 | const WORKSPACE_OPTIONS = [ |
| 2 | { label: 'Operations workspace', value: 'ops' }, |
| 3 | { label: 'Revenue cockpit', value: 'revenue' }, |
| 4 | { label: 'Support command center', value: 'support' }, |
| 5 | ] |
| 6 | |
| 7 | const schema = { |
| 8 | workspace: field.select('Workspace') |
| 9 | .options(WORKSPACE_OPTIONS) |
| 10 | .defaultSelected('revenue') |
| 11 | .required(), |
| 12 | } |
Numeric option values
SeatPack.tsxtsx
| 1 | const schema = { |
| 2 | seatPack: field.select('Seat pack') |
| 3 | .options([ |
| 4 | { label: 'Starter · 5 seats', value: 5 }, |
| 5 | { label: 'Growth · 12 seats', value: 12 }, |
| 6 | { label: 'Scale · 25 seats', value: 25 }, |
| 7 | ]) |
| 8 | .defaultSelected(12), |
| 9 | } |
Async city search with debounce
CitySearch.tsxtsx
| 1 | const schema = { |
| 2 | city: field.select('City') |
| 3 | .optionsFrom(fetchCities, { |
| 4 | key: 'city-search', |
| 5 | debounce: 250, |
| 6 | minChars: 2, |
| 7 | }) |
| 8 | .searchable(), |
| 9 | } |
Dependent cascading selects
RegionCascade.tsxtsx
| 1 | const schema = { |
| 2 | country: field.select('Country').options(COUNTRIES).required(), |
| 3 | region: field.select('Region') |
| 4 | .optionsFrom(fetchRegions, { |
| 5 | key: 'regions', |
| 6 | dependsOn: ['country'], |
| 7 | }), |
| 8 | } |
Fully custom trigger
CustomTrigger.tsxtsx
| 1 | const schema = { |
| 2 | routingMode: field.select('Routing mode') |
| 3 | .options(ROUTING_MODES), |
| 4 | } |
| 5 | |
| 6 | const form = useFormBridge(schema) |
| 7 | const controller = form.fieldController('routingMode') |
| 8 | |
| 9 | <form.Form onSubmit={save}> |
| 10 | <MyTrigger |
| 11 | label={controller.value ?? 'Select mode'} |
| 12 | onClick={controller.openPicker} |
| 13 | /> |
| 14 | <MyModal |
| 15 | open={controller.isOpen} |
| 16 | options={controller.options} |
| 17 | onSelect={controller.setValue} |
| 18 | onClose={controller.closePicker} |
| 19 | /> |
| 20 | </form.Form> |