webdevpuneet.com

Flight Search Form — Travel Booking HTML CSS JS

Flight Search Form · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="fsf-card">
  <div class="fsf-tabs">
    <button type="button" class="fsf-tab active" id="fsfTabRound" onclick="fsfSetTrip('round')">Round trip</button>
    <button type="button" class="fsf-tab" id="fsfTabOne" onclick="fsfSetTrip('one')">One way</button>
  </div>

  <div class="fsf-row fsf-locations">
    <div class="fsf-field">
      <label for="fsfFrom">From</label>
      <input type="text" id="fsfFrom" value="New York (JFK)">
    </div>
    <button type="button" class="fsf-swap" id="fsfSwap" aria-label="Swap origin and destination">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
    </button>
    <div class="fsf-field">
      <label for="fsfTo">To</label>
      <input type="text" id="fsfTo" value="London (LHR)">
    </div>
  </div>

  <div class="fsf-row fsf-dates">
    <div class="fsf-field">
      <label for="fsfDepart">Depart</label>
      <input type="date" id="fsfDepart">
    </div>
    <div class="fsf-field" id="fsfReturnField">
      <label for="fsfReturn">Return</label>
      <input type="date" id="fsfReturn">
    </div>
    <div class="fsf-field fsf-pax-field">
      <label>Travelers</label>
      <button type="button" class="fsf-pax-btn" id="fsfPaxBtn">1 adult <span class="fsf-caret">▾</span></button>
      <div class="fsf-pax-pop" id="fsfPaxPop">
        <div class="fsf-pax-row">
          <div>
            <div class="fsf-pax-label">Adults</div>
            <div class="fsf-pax-sub">Age 18+</div>
          </div>
          <div class="fsf-stepper">
            <button type="button" onclick="fsfStep('adults',-1)" aria-label="Decrease adults">−</button>
            <span id="fsfAdults">1</span>
            <button type="button" onclick="fsfStep('adults',1)" aria-label="Increase adults">+</button>
          </div>
        </div>
        <div class="fsf-pax-row">
          <div>
            <div class="fsf-pax-label">Children</div>
            <div class="fsf-pax-sub">Age 2–17</div>
          </div>
          <div class="fsf-stepper">
            <button type="button" onclick="fsfStep('children',-1)" aria-label="Decrease children">−</button>
            <span id="fsfChildren">0</span>
            <button type="button" onclick="fsfStep('children',1)" aria-label="Increase children">+</button>
          </div>
        </div>
        <button type="button" class="fsf-pax-done" id="fsfPaxDone">Done</button>
      </div>
    </div>
  </div>

  <p class="fsf-error" id="fsfError" hidden></p>
  <button type="button" class="fsf-submit" id="fsfSubmit">
    <svg class="fsf-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" hidden><circle cx="12" cy="12" r="9" stroke-opacity=".25"/><path d="M21 12a9 9 0 0 0-9-9"/></svg>
    <span id="fsfSubmitLabel">Search flights</span>
  </button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Round trip / one way toggle
A pill tab switch hides and disables the return-date field for one-way trips without shifting layout.
One-click origin/destination swap
A circular button exchanges the From/To values with a rotating icon transform.
Passenger stepper popover
Adults and children counters with floor/ceiling limits and a live computed summary label.
Outside-click and Done-button dismissal
The traveler popover closes on an outside click or an explicit Done button — standard popover UX.
Ordered field validation
Checks empty fields, identical origin/destination, missing departure date, and invalid return date in sequence.
Simulated async search state
The submit button shows a spinning icon, then a checkmark, mimicking a real network request.
Sensible default dates
Departure defaults two weeks out and return one week after that, so the dates are never blank or arbitrary.
Animation-safe popover
Opacity and transform-only transitions keep the traveler popover smooth across every framework export.

About this UI Snippet

Flight Search Form — Origin/Destination Swap, Passenger Stepper & Date Validation

Screenshot of the Flight Search Form snippet rendered live

Every travel booking site opens with the same widget: an origin/destination pair, departure and return dates, a passenger count, and a search button — yet getting the small interactions right (swapping cities, picking a traveler count without a clunky native select, validating dates) is what separates a form that feels professional from one that feels like a prototype. This snippet builds the full pattern in plain HTML, CSS, and vanilla JavaScript.

Round trip vs. one way

A pill-style tab toggle switches between "Round trip" and "One way". Choosing one way hides the return-date field by setting its visibility to hidden (not display: none, which would reflow the row and shift the passenger field) and disables its input so a hidden date can't be submitted. This keeps the three-column date row visually stable while the modes change.

The swap button

Between the From and To fields, a circular button swaps the two input values directly — a one-line value exchange — and adds a .spun class that rotates the icon 180° via a CSS transform transition, removed after 250ms with setTimeout. Because only transform is animated, the rotation is GPU-friendly and converts safely to every export target.

Passenger stepper popover

Rather than a native <select> (which can't show "Adults" and "Children" as separate counters), the traveler field opens a popover with two stepper rows, each with decrement/increment buttons bound to a shared fsfStep(key, delta) function. The function enforces a floor of one adult, a floor of zero children, and a combined cap of eight travelers, then updates both the visible counts and a computed summary label ("2 adults, 1 child") on the trigger button. The popover animates in with opacity and transform: translateY() scale() only — never height — so it stays smooth after every framework export. A document-level click listener closes it when you click outside, and a "Done" button closes it explicitly.

Submit validation

Clicking Search runs through ordered checks — both fields filled, origin and destination not identical (case-insensitive), a departure date chosen, and (in round-trip mode) a return date on or after departure — surfacing the first failing message in a red error line above the button. Passing validation triggers a simulated search: the button disables, shows a spinning ring icon and "Searching…", then resolves to a checkmark label before resetting, so the interaction reads as asynchronous even without a real API call.

Sensible defaults

On load, the departure date defaults to two weeks out and the return date to one week after that, so the form never starts on today's date (which would make the date pickers feel arbitrary) and demonstrates working dates immediately.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA flight search card appears with round-trip/one-way tabs, From/To fields, dates, and a traveler stepper.
  2. 2
    Swap origin and destinationClick the circular arrow button between the fields — the two values exchange instantly with a rotating icon.
  3. 3
    Open the traveler popoverClick the "1 adult" button. Use the +/− steppers to adjust adults and children; the label updates live.
  4. 4
    Switch trip typeClick "One way" to hide and disable the return date field; click "Round trip" to bring it back.
  5. 5
    Submit and see validationClick Search with empty or identical fields to see the inline error message; fix it and the button proceeds.
  6. 6
    Wire up real flight dataReplace the simulated setTimeout in the submit handler with a fetch to your flight-search API and render results below the form.

Real-world uses

Common Use Cases

Airline and OTA booking sites
The standard above-the-fold search widget for flight booking and comparison platforms.
Travel agency landing pages
A credible, interactive search form that signals "real booking engine" rather than a static mockup.
Hotel and car-rental adaptation
Swap the From/To fields for Pickup/Drop-off or Check-in/Check-out — the stepper and validation pattern carries over.
Multi-step booking funnels
Use this as step one, then pass the validated origin, destination, dates, and passenger count to a multi-step form.
Travel app dashboards
Pair with a calendar widget for date selection or a country selector for nationality fields.
Learning popover and stepper patterns
A practical reference for outside-click dismissal, transform-only animation, and floor/ceiling counters.
Related: Loan EMI Calculator
See the Loan EMI Calculator for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Replace the setTimeout block in the submit handler with a fetch call passing the from, to, depart, return, and pax values; render the returned flight list below the form on success and show the API's error message in the existing #fsfError element on failure.

Attach an input listener to the From/To fields that filters a static airport array and renders a positioned dropdown below the field, similar to the suggestion-list pattern in an address autocomplete field, then set the input's value on selection.

Add a third tab "Multi-city" that renders a repeatable list of From/To/date row groups with an "Add another flight" button; reuse the existing swap and validation logic per row.

Tailwind's default transition utility (and most framework export targets) only reliably animates transform, opacity, and a few color properties. Animating height or visibility directly causes the popover to snap instead of glide after conversion, so this snippet sticks to transform/opacity for guaranteed cross-framework smoothness.

In React, hold trip, pax, and field values in useState and derive the passenger label with useMemo; in Vue, use ref()/computed(); in Angular, use component fields and a getter. The validation order and popover open/close logic translate directly into each framework's event handlers.