Select
Native select component with data-driven options, sizing variants, and multi-select support.
Overview
Select is a styled wrapper around a native <select> element, applying the same .form-input class and shared label/help/validation-feedback wiring as other form leaves — see Form Field for the underlying pattern. Pass an options array for data-driven rendering — plain strings, or objects conforming to SelectOptionDef (label, value, disabled, selected) — or omit it and write native <option> elements directly as children instead. An object option with no value renders without a value attribute, so the browser falls back to its text content.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select
placeholder="Choose an option"
aria-label="Basic select example"
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3', disabled: true }
]}
/>
)
}As a form field
Pass label / help / invalid / invalidFeedback / valid / validFeedback directly to Select to render it together with an associated FormLabel, FormHelp, and FormFeedback, already wired up via aria-describedby and aria-invalid — see Form Field instead when a control needs to be grouped with companion content under one label.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select
label="Country"
help="Used to calculate shipping costs."
options={[
{ label: 'Australia', value: 'au' },
{ label: 'Canada', value: 'ca' },
{ label: 'United Kingdom', value: 'gb' },
{ label: 'United States', value: 'us' }
]}
/>
)
}Native options
Omit the options prop and write native <option> elements directly as children when the list doesn't need to be data-driven — every attribute a plain <option> supports (value, disabled, selected, etc.) works exactly as it would outside Select.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select aria-label="Native options example">
<option>Choose an option</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3" disabled>
Three
</option>
</Select>
)
}Sizing
Set the size prop to sm or lg to change the control's padding, typography, and border radius — matching same-sized Text Input controls alongside it.
import { Select } from '@chassis-ui/react'
export const Example = () => {
const options = [
{ label: 'Choose an option', value: '' },
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3', disabled: true }
]
return (
<>
<Select size="lg" aria-label="Large select example" options={options} />
<Select aria-label="Default select example" options={options} />
<Select size="sm" aria-label="Small select example" options={options} />
</>
)
}Disabled
Add the disabled boolean attribute to give the control a muted color. Browsers also remove disabled controls from the tab order and from form submission automatically.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select
placeholder="Choose an option"
aria-label="Disabled select example"
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3', disabled: true }
]}
disabled
/>
)
}Multiple selection
Set the multiple boolean prop to allow more than one option to be selected at once — pass value as a string array to control the selection from state, or see Default selection below for setting more than one option selected upfront without controlling value at all.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select
aria-label="Multiple select example"
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3' }
]}
multiple
/>
)
}Select size
The native size attribute — how many options show without scrolling, distinct from the sm/lg size prop above — is exposed as htmlSize instead, since SelectProps omits the native size attribute from its own type to avoid a name clash with the visual size prop.
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<Select
aria-label="Select size example"
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3' }
]}
htmlSize={3}
/>
)
}Default selection
Mark one or more options entries selected: true to set the initial value without passing value/defaultValue directly — it's resolved into the select's own defaultValue internally, so it's ignored whenever value/defaultValue is also set. Mark more than one entry selected only when multiple is also set; otherwise, matching native <select> behavior, only the last selected entry wins (and a console warning flags the mistake in development).
import { Select } from '@chassis-ui/react'
export const Example = () => {
return (
<>
<Select
aria-label="Single selection example"
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2', selected: true },
{ label: 'Three', value: '3' }
]}
/>
<Select
aria-label="Multiple selection example"
multiple
options={[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2', selected: true },
{ label: 'Three', value: '3', selected: true }
]}
/>
</>
)
}Controlled value
Pass value and onChange to control the selected option from external state. Unlike Text Input, onChange here isn't normalized to hand back the new value directly — it receives the native change event, the same as passing onChange to a plain <select>.
import { useState } from 'react'
import { Select } from '@chassis-ui/react'
export const Example = () => {
const [value, setValue] = useState('js')
const options = [
{ label: 'JavaScript', value: 'js' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' }
]
const selection = options.find((o) => o.value === value)?.label || 'None'
return (
<Select
aria-label="Controlled select example"
value={value}
onChange={(e) => setValue(e.target.value)}
help={`Current selection: ${selection}`}
options={options}
/>
)
}Adorns
Pass adornStart and/or adornEnd to render content — an icon, text, or an actionable button —
at the leading or trailing edge of the select. Either prop switches the rendered markup from a bare
<select> to chassis-css's input help pattern:
a .form-input wrapper containing the adorn(s) around a .ghost-input-classed <select>. Wrap
adorn content in InputAdorn so it picks up the .input-adorn class the wrapper's flex layout
targets — see Text Input for the same pattern on a text field.
import { Icon, InputAdorn, Select } from '@chassis-ui/react'
export const Example = () => {
const options = [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'py' }
]
return (
<Select
aria-label="Language"
placeholder="Choose a language"
options={options}
adornStart={
<InputAdorn>
<Icon name="search-outline" size={16} />
</InputAdorn>
}
adornEnd={<InputAdorn>Required</InputAdorn>}
/>
)
}A native <select> normally opens its dropdown on click no matter where the pointer lands, since
it fills its own box completely. Once wrapped, the select only occupies the space between its
adorns, so Select adds a click handler to the wrapper itself — clicking an adorn (or the
wrapper's own padding) focuses the select and calls showPicker() to open it, the same as clicking
the select directly. Clicking an actionable adorn (InputAdorn with component="button" or
component="a") is left alone, since that adorn already handles its own click.
showPicker() isn't available in every browser. Where it's missing, the click still moves focus
to the select, leaving it reachable by keyboard (Space/Down to open) — the same fallback a bare,
unwrapped select already relies on for non-pointer input.
A wrapped select with multiple or an htmlSize greater than 1 renders as an inline listbox
rather than a dropdown, so it has no picker to open — Select omits .form-caret and the
click-to-open handler for those cases, leaving the listbox itself directly clickable as normal.
API
Select
| Prop | Type | Default |
|---|---|---|
adornEnd? | ReactNode | — |
Content rendered at the select's trailing edge, e.g. a InputAdorn icon, text, or button.
Setting either adornStart or adornEnd renders a .form-input wrapper around a
.ghost-input, matching chassis-css's [input help](https://chassis-ui.com/css/docs/forms/input-adorn) pattern.
Clicking anywhere in the wrapper (other than an actionable adorn) opens the select, since the
native element itself no longer fills the wrapper's full width. | ||
adornStart? | ReactNode | — |
Content rendered at the select's leading edge, e.g. a InputAdorn icon, text, or button.
Setting either adornStart or adornEnd renders a .form-input wrapper around a
.ghost-input, matching chassis-css's [input help](https://chassis-ui.com/css/docs/forms/input-adorn) pattern.
Clicking anywhere in the wrapper (other than an actionable adorn) opens the select, since the
native element itself no longer fills the wrapper's full width. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
help? | ReactNode | — |
| A description for the field, rendered below the select. | ||
htmlSize? | number | — |
| Specifies the number of visible options in a drop-down list. | ||
invalid? | boolean | — |
Set component validation state to invalid. Left unset, the component shows as invalid by itself
from a submit it held back (an empty required one, say) until it no longer would, or its
form is reset. Set it, to false too, and it alone decides. | ||
invalidFeedback? | ReactNode | — |
An error message for the field, rendered below the select when invalid is set, and after a
submit the field held back, until it no longer would. | ||
label? | ReactNode | — |
The field's caption, rendered as a FormLabel associated with this select. | ||
multiple? | boolean | — |
Allows more than one option to be selected at once. Pass value as a string array (or set
selected on more than one options entry) to control the selection. | ||
onChange? | ChangeEventHandler<HTMLSelectElement> | — |
Method called immediately after the value prop changes. | ||
options? | SelectOptionDef[] | string[] | — |
Options list of the select component. Available keys: label, value, disabled,
selected.
Examples:
- options={[{ value: 'js', label: 'JavaScript' }, { value: 'html', label: 'HTML', disabled: true }]}
- options={['js', 'html']} | ||
placeholder? | string | — |
Renders a disabled placeholder option as the first item (e.g. "Select a country…").
The option has an empty value so it is not selectable once another option is chosen. A
single select with no value, defaultValue or selected option starts on it, so its value
is '' and a required one holds its form back until an option is chosen. | ||
size? | "sm" | "lg" | — |
| Size the component sm or lg. | ||
valid? | boolean | — |
| Set component validation state to valid. | ||
validFeedback? | ReactNode | — |
A success message for the field, rendered below the select when valid is set. | ||
value? | string | number | string[] | — |
The value attribute of component.
@controllable onChange | ||
SelectOptionDef
Shape of each entry in Select's options array (data-driven rendering).
| Prop | Type | Default |
|---|---|---|
disabled? | boolean | — |
| Marks the option as disabled — it can't be clicked or reached via the keyboard. | ||
label? | string | — |
Label text rendered inside the option. Falls back to the browser's default (the value,
stringified) when omitted. | ||
selected? | boolean | — |
Marks the option as selected by default (uncontrolled) — resolved into the select's own
defaultValue, and ignored when the select's value/defaultValue is set directly. Set on
more than one option only when multiple is also set on Select — otherwise, matching
native <select> behavior, only the last option with selected set wins. | ||
value? | string | number | — |
| The option's value attribute. | ||