Skip to main content
Version: Next 🚧

Flight search form

A booking form like the ones on airline and travel sites. The airports come from a server: the list starts empty and asks for a city, airport or code, then searches as the user types, with a loader. Trip type and cabin are segmented controls, the dates switch between one date and a range with the trip type, and Search flights checks everything before it goes.

Try it: open From (a dropdown on the web, a bottom sheet on the iOS and Android tabs): the list is empty and says Type a city, airport or code. Type lon: the loader shows Finding airports… while the fake server answers, then London's two airports appear. Type xyz and it says No airport matches.

Loading…
<View style={styles.form}>
  <HorizontalList items={TRIPS} value={trip} onChange={setTrip} accessibilityLabel="Trip type" />

  <View>
    <SelectPicker
      label="From"
      placeholder="City or airport"
      loadOptions={searchAirports}
      value={from}
      onChange={setFrom}
      searchPrompt="Type a city, airport or code"
      loadingText="Finding airports…"
      emptyText="No airport matches"
    />
    <FieldError message={submitted && errors.from} />
  </View>

  <View>
    <SelectPicker
      label="To"
      placeholder="City or airport"
      loadOptions={searchAirports}
      value={to}
      onChange={setTo}
      searchPrompt="Type a city, airport or code"
      loadingText="Finding airports…"
      emptyText="No airport matches"
    />
    <FieldError message={submitted && errors.to} />
  </View>

  <View>
    {trip === 'round' ? (
      <DatePickerField
        label="Dates"
        placeholder="Depart → Return"
        mode="range"
        value={dates}
        onConfirm={setDates}
        minDate={new Date()}
      />
    ) : (
      <DatePickerField label="Date" placeholder="Depart" value={depart} onConfirm={setDepart} minDate={new Date()} />
    )}
    <FieldError message={submitted && errors.dates} />
  </View>

  <WheelPickerField label="Travellers" items={TRAVELLERS} value={travellers} onConfirm={setTravellers} />

  <View>
    <Text style={[styles.label, { color: colors.textSecondary }]}>Cabin</Text>
    <HorizontalList items={CABINS} value={cabin} onChange={setCabin} accessibilityLabel="Cabin" />
  </View>

  <Pressable
    onPress={submit}
    style={[styles.button, { borderRadius: radius.md, backgroundColor: colors.primary }]}
    accessibilityRole="button"
  >
    <Text style={[styles.buttonText, { color: colors.onPrimary }]}>Search flights</Text>
  </Pressable>
  {summary ? <Text style={[styles.summary, { color: colors.text }]}>✈️ {summary}</Text> : null}
</View>

How it's built​

  • Airports from a server. loadOptions={searchAirports} does the searching: it waits for a pause in typing, shows loadingText from the keystroke until the airports arrive, and ignores answers to older queries.
  • The right text at each step. Before typing, the empty list shows searchPrompt; after a search with no results, emptyText. To list something before typing instead (popular or recent airports), pass them as options.
  • Search by anything. The server matches city, code and airport name, so tlv, heathrow and new york all work. The list shows what it returns without filtering again.
  • The picked airport keeps its label after a later search no longer returns it.
  • Trip type is a segmented HorizontalList; it swaps the dates field between a range and a single date. Cabin is another, with four options that scroll if the screen is narrow.
  • Theme colours. The labels, errors and Search flights read usePickerTheme(), so the form matches the pickers in dark mode and in each design; Material's fields float their labels, the others show them above.
  • Validation runs on submit and shows a message under each field, including From and To being the same airport.

With your API​

Replace the stand-in with a real request. loadOptions takes any function that returns a promise of options:

const searchAirports = async (query: string) => {
const res = await fetch(`https://api.example.com/airports?q=${encodeURIComponent(query)}`);
if (!res.ok) throw new Error(`Airport search failed: ${res.status}`);
const airports: Airport[] = await res.json();
return airports.map(toOption);
};

A rejected promise shows emptyText. Set searchDebounce (300 ms by default) higher for a slow or rate-limited API.

In a form library, wire the fields the same way as in the forms guide: value={field.value} and onChange={field.onChange} on the SelectPicker, onConfirm={field.onChange} on the fields.