Skip to main contentSkip to docs navigation

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.

JSX
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.

Used to calculate shipping costs.
JSX
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.

JSX
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.

JSX
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.

JSX
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.

JSX
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.

JSX
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).

JSX
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>.

Current selection: JavaScript
JSX
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.

Required
JSX
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

PropTypeDefault
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).

PropTypeDefault
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.