Components
Select
BetaA custom dropdown component with keyboard navigation, validation states, and support for icons and disabled options.
Purpose
Select replaces the browser's native <select> element with a fully styled, keyboard-accessible custom dropdown. It is used whenever travellers need to choose a single value from a list — picking a destination region, cabin class, board basis, or trip duration. The component preserves all native accessibility semantics while matching the Kuoni visual language.
Usage
Use Select when the list has more than 4 options, when options may carry icons or disabled states, or when the choice is part of a form that requires inline validation feedback. For 2–4 options, prefer Radio Button instead — it shows every choice at a glance with no extra click. For a single binary choice, use Checkbox or Toggle rather than a two-option Select.
Select appears inside forms — booking flows, enquiry forms, and account preference screens — wherever a single value must be picked from a bounded list.
Anatomy
- Label
- Trigger button
- Placeholder
- Chevron icon
- Dropdown panel
- Option item
- Selection indicator
- Help text
- Error message
Variants
Default
The standard single-selection dropdown with a placeholder.
With help text
Provide a helpText prop to guide the traveller before they interact.
Prices are shown per person based on two adults sharing.
With icons
Options can carry a leading icon node — useful for flag or category icons.
With disabled options
Pass disabled: true on individual option objects to prevent selection.
Greyed options are unavailable for your selected dates.
Error state
Pass an error string to show inline validation feedback and switch the border to danger colour.
Please select a duration to continue.
Disabled
The entire field is non-interactive when disabled is true.
States
| State | Trigger button appearance | Dropdown panel |
|---|---|---|
| Default (empty) | Grey-300 border, placeholder text in grey-400 | Closed |
| Default (value set) | Grey-300 border, selected label in grey-900 | Closed |
| Hover | Teal border | Closed |
| Focus | Teal border + ring-2 ring-teal | Closed |
| Open | Teal border + ring-2 ring-teal, chevron rotated 180 deg | Visible |
| Option hovered | — | Row background bg-teal-50 |
| Option selected | — | Teal check icon on the row |
| Error | Danger border, error text below | Closed |
| Disabled | Grey-100 background, grey-400 text, not-allowed cursor | Cannot open |
Best practices
Label the field with the decision the traveller is making, not a generic word like 'Select'.
<Select label="Destination region" options={regions} />
A placeholder disappears on selection and is never read reliably by screen readers as a label.
<Select placeholder="Destination region" options={regions} />
When the traveller might not know what a field means, add a helpText explanation.
<Select
label="Board basis"
helpText="Board basis determines which meals are included."
options={boardBasis}
/>
Tell the traveller what they need to do, not just that something went wrong.
// Avoid
<Select label="Cabin class" error="Invalid selection." options={cabins} />
// Prefer
<Select label="Cabin class" error="Please select a cabin class to see prices." options={cabins} />
Use required so the field is included in native form validation and screen readers announce it.
<Select label="Number of travellers" required options={travellers} />
Disabled options confuse travellers. Pair them with help text explaining why they are unavailable.
// Avoid
<Select label="Room type" options={[{ value: 'suite', label: 'Suite', disabled: true }]} />
// Prefer
<Select
label="Room type"
helpText="Suites are unavailable for your selected dates."
options={[{ value: 'suite', label: 'Suite', disabled: true }]}
/>
Select keeps long lists scannable without taking up layout space.
<Select label="Departure airport" options={twentyAirports} />
For 2–4 options prefer Radio or ButtonGroup — they show all choices at a glance with no extra click.
// Avoid for a 2-option list
<Select label="Adults" options={[{ value: '1', label: '1' }, { value: '2', label: '2' }]} />
Props
Select
| Prop | Type | Default | Description |
|---|---|---|---|
| options* | Array<{ value: string; label: string; disabled?: boolean; icon?: ReactNode }> | — | The list of options to display in the dropdown panel. |
| label | string | — | Visible label rendered above the trigger button and linked via htmlFor. |
| id | string | — | HTML id applied to the trigger button. Auto-generated if omitted. |
| value | string | — | Controlled selected value. Must match one of the option values. |
| defaultValue | string | — | Uncontrolled initial value. Use when you do not need to control state externally. |
| onChange | (value: string, option: Option) => void | — | Callback fired when the user selects an option. Receives the value string and the full option object. |
| placeholder | string | 'Select an option' | Text shown in the trigger when no value is selected. |
| disabled | boolean | false | When true, the entire field is non-interactive. |
| required | boolean | false | Marks the field as required for form validation and accessibility. |
| error | string | — | Validation error message. Switches the border to danger colour and displays text below the trigger. |
| helpText | string | — | Supplementary guidance displayed below the trigger. Hidden when error is set. |
| className | string | — | Additional CSS classes applied to the outermost wrapper element. |
Accessibility
Keyboard interaction
| Key | Action |
|---|---|
| Enter or Space | Opens the dropdown when trigger is focused; selects the highlighted option when panel is open |
| Arrow Down | Moves focus to the next option; opens the panel if closed |
| Arrow Up | Moves focus to the previous option |
| Home | Moves focus to the first option |
| End | Moves focus to the last option |
| Escape | Closes the panel and returns focus to the trigger |
| Tab | Closes the panel and moves focus to the next focusable element |
ARIA
- The trigger button has
role="combobox"witharia-expanded,aria-haspopup="listbox", andaria-controlspointing to the panel. - The panel has
role="listbox". - Each option has
role="option"andaria-selected. - Disabled options have
aria-disabled="true"and are skipped during keyboard navigation. - When
erroris set, the error element hasrole="alert"and the trigger references it viaaria-describedby. - When
requiredis true, the trigger hasaria-required="true".
Related components
- Sort Dropdown — a purpose-built compact single-select for reordering results, sharing Select's listbox interaction pattern.
- Radio Button — the better choice for short lists (2–4 options) where showing all choices at once is preferable to a dropdown.
- Checkbox — for multi-select or standalone binary choices rather than a single value from a list.