KKUONIDesign System

Components

Select

Beta

A 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

  1. Label
  2. Trigger button
  3. Placeholder
  4. Chevron icon
  5. Dropdown panel
  6. Option item
  7. Selection indicator
  8. Help text
  9. 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.

Disabled

The entire field is non-interactive when disabled is true.

States

StateTrigger button appearanceDropdown panel
Default (empty)Grey-300 border, placeholder text in grey-400Closed
Default (value set)Grey-300 border, selected label in grey-900Closed
HoverTeal borderClosed
FocusTeal border + ring-2 ring-tealClosed
OpenTeal border + ring-2 ring-teal, chevron rotated 180 degVisible
Option hoveredRow background bg-teal-50
Option selectedTeal check icon on the row
ErrorDanger border, error text belowClosed
DisabledGrey-100 background, grey-400 text, not-allowed cursorCannot open

Best practices

Do

Label the field with the decision the traveller is making, not a generic word like 'Select'.

<Select label="Destination region" options={regions} />
Don't

A placeholder disappears on selection and is never read reliably by screen readers as a label.

<Select placeholder="Destination region" options={regions} />
Do

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}
/>
Don't

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} />
Do

Use required so the field is included in native form validation and screen readers announce it.

<Select label="Number of travellers" required options={travellers} />
Don't

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 }]}
/>
Do

Select keeps long lists scannable without taking up layout space.

<Select label="Departure airport" options={twentyAirports} />
Don't

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

PropTypeDefaultDescription
options*Array<{ value: string; label: string; disabled?: boolean; icon?: ReactNode }>The list of options to display in the dropdown panel.
labelstringVisible label rendered above the trigger button and linked via htmlFor.
idstringHTML id applied to the trigger button. Auto-generated if omitted.
valuestringControlled selected value. Must match one of the option values.
defaultValuestringUncontrolled initial value. Use when you do not need to control state externally.
onChange(value: string, option: Option) => voidCallback fired when the user selects an option. Receives the value string and the full option object.
placeholderstring'Select an option'Text shown in the trigger when no value is selected.
disabledbooleanfalseWhen true, the entire field is non-interactive.
requiredbooleanfalseMarks the field as required for form validation and accessibility.
errorstringValidation error message. Switches the border to danger colour and displays text below the trigger.
helpTextstringSupplementary guidance displayed below the trigger. Hidden when error is set.
classNamestringAdditional CSS classes applied to the outermost wrapper element.

Accessibility

Keyboard interaction

KeyAction
Enter or SpaceOpens the dropdown when trigger is focused; selects the highlighted option when panel is open
Arrow DownMoves focus to the next option; opens the panel if closed
Arrow UpMoves focus to the previous option
HomeMoves focus to the first option
EndMoves focus to the last option
EscapeCloses the panel and returns focus to the trigger
TabCloses the panel and moves focus to the next focusable element

ARIA

  • The trigger button has role="combobox" with aria-expanded, aria-haspopup="listbox", and aria-controls pointing to the panel.
  • The panel has role="listbox".
  • Each option has role="option" and aria-selected.
  • Disabled options have aria-disabled="true" and are skipped during keyboard navigation.
  • When error is set, the error element has role="alert" and the trigger references it via aria-describedby.
  • When required is true, the trigger has aria-required="true".
  • 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.