Flight Card
ProA flight an assistant found or booked, inside a chat reply: the airline, both ends of the route with the line and stops between them, the fare, and once it is booked, the live status, gate and seat.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Times, dates and prices go in already formatted, in the airport's local time. Format them where you know the traveller's locale and the airport's time zone, with Intl.DateTimeFormat("en-US", { timeZone: "America/Denver" }), so the server and the browser print the same thing.
Anatomy#
A header with the airline, the route as the core, and a footer with the fare and the actions. Every part is droppable: a result row needs no actions, a status update needs no price.
import {
FlightCardList,
FlightCard,
FlightCardHeader,
FlightCardAirline,
FlightCardTag,
FlightCardStatus,
FlightCardLeg,
FlightCardRoute,
FlightCardEndpoint,
FlightCardPath,
FlightCardStop,
FlightCardDetails,
FlightCardDetail,
FlightCardFooter,
FlightCardPrice,
FlightCardActions,
FlightCardSkeleton,
} from "@/components/ui/flight-card"
<FlightCardList>
<FlightCard>
<FlightCardHeader>
<FlightCardAirline />
<FlightCardTag />
<FlightCardStatus />
</FlightCardHeader>
<FlightCardLeg>
<FlightCardRoute>
<FlightCardEndpoint />
<FlightCardPath>
<FlightCardStop />
</FlightCardPath>
<FlightCardEndpoint />
</FlightCardRoute>
</FlightCardLeg>
<FlightCardDetails>
<FlightCardDetail />
</FlightCardDetails>
<FlightCardFooter>
<FlightCardPrice />
<FlightCardActions />
</FlightCardFooter>
</FlightCard>
<FlightCardSkeleton />
</FlightCardList>Stops and legs#
Each FlightCardStop sets its airport code on the line, and the caption under it counts them: “Nonstop”, “1 stop · 1h 05m”, “2 stops”. A red-eye that lands the next day takes dayOffset={1} and prints a small +1 after the time. For a round trip, wrap each direction in a FlightCardLeg, as in the card at the top of the page: the legs part with a hairline and share one price.
Results list#
When the assistant finds several, put them in a FlightCardList. They share one frame and part with hairlines. Give the list a value and each card becomes one option of a radio group: click anywhere on a card, or move with the arrow keys. The picked card fills its circle in ink and takes a faint wash, so the choice reads without turning the list into a rainbow.
Booked flight and status#
Once it is booked, set status on the root and drop in FlightCardStatus: on time, delayed, boarding, departed, landed or cancelled, worded and toned for you. A delay keeps the scheduled time struck through above the new one, which turns amber; a cancellation strikes both times through. FlightCardDetails holds the booking facts a traveller looks for at the airport.
- Terminal
- 1
- Gate
- B44
- Seat
- 32K
- Terminal
- 7
- Gate
- 71A
- Seat
- 14C
- Terminal
- 2
- Gate
- D7
- Seat
- 24A
- Terminal
- 1
- Gate
- 12
- Seat
- Open
In a conversation#
The card sits in the assistant's reply at the column's width, under the sentence that explains it. The route is a grid with the path as the flexible column, so the times keep their size and never wrap in a 360px column; the path and the city give way first.
- Booking
- K7XQ2P
- Terminal
- 2
- Gate
- D7
- Seat
- 24A
For a search, echo the query back as chips above the list so the traveller can see what was searched before they compare.
Loading#
While results stream in, FlightCardSkeleton holds the shape of a row inside the same list, so the frame and its hairlines are already in place when the flights arrive.
API reference#
FlightCard
One flight or one itinerary, an <article>, and the context every part reads. Every part accepts className, merged last, and forwards its element's props. flightCardVariants is the recipe.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "on-time" | "delayed" | "boarding" | "departed" | "landed" | "cancelled" | - | Where a booked flight stands. Words and tones FlightCardStatus; cancelled strikes the times through. Leave it out for a search result. |
| value | string | - | Inside a selectable FlightCardList, the value this card stands for. The whole card becomes the radio. |
| disabled | boolean | - | A selectable card that cannot be picked (sold out). |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card". |
| data-status | The status. |
| data-selected | Present on the picked card of a selectable list. |
FlightCardHeader
The top line, a <div>: the airline first, then whatever follows it pushed to the end (a tag, the status, a price). In a selectable list it ends with the radio circle.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-header"; the circle is "flight-card-control". |
FlightCardAirline
Who flies it: a 32px Avatar with the airline's code as the fallback, the name, and the flight number under it.
| Prop | Type | Default | Description |
|---|---|---|---|
| name* | React.ReactNode | - | The airline. |
| code | string | - | The two-letter IATA code, shown in the mark until a logo loads or when there is none. |
| src | string | - | A logo image. Use your own licensed assets. |
| flight | React.ReactNode | - | The second line: flight number, aircraft, cabin. |
| color | AvatarProps["color"] | - | Tints the fallback mark. |
| children | React.ReactNode | - | Replaces the code in the mark, for a glyph. |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-airline". |
FlightCardTag
Why this result stands out, a small soft Badge. Forwards Badge props.
| Prop | Type | Default | Description |
|---|---|---|---|
| kind | "best" | "cheapest" | "fastest" | - | A preset wording and tone. |
| children | React.ReactNode | - | Your own wording. |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-tag". |
| data-kind | The kind. |
FlightCardStatus
A dot Badge worded and toned from the root's status. Forwards Badge props.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | BadgeProps["variant"] | - | Overrides the tone the status picks. |
| children | React.ReactNode | - | Overrides the wording ("Delayed 50 min"). |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-status". |
FlightCardLeg
One direction of an itinerary, a <section>. Legs that follow each other part with a hairline, so outbound and return read as one card.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | - | The caption above it ("Outbound · Thu, Jun 18"). |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-leg". |
FlightCardRoute
Departure, path and arrival on one shared grid, so the times, the airport codes and the path's caption line up row by row. A <div>.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-route". |
FlightCardEndpoint
One end of the route. The first sits on the start edge, the last on the end edge. The time is 18px semibold in tabular-nums and never wraps.
| Prop | Type | Default | Description |
|---|---|---|---|
| time* | React.ReactNode | - | The local time, already formatted. |
| code | React.ReactNode | - | The airport code (LAX). |
| city | React.ReactNode | - | The city, truncated under the code. |
| date | React.ReactNode | - | A small date above the time, for itineraries. |
| was | React.ReactNode | - | The scheduled time, struck through above a changed one. The new time turns amber. |
| dayOffset | number | - | Days after departure: 1 prints +1 and reads "next day". |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-endpoint". |
FlightCardPath
The line between the two ends, with the duration above and the stops on it. With no stops it reads "Nonstop", with one "1 stop · 1h 05m", with more "2 stops". Stops must be direct children.
| Prop | Type | Default | Description |
|---|---|---|---|
| duration | React.ReactNode | - | Total travel time, above the line. |
| caption | React.ReactNode | - | Replaces the stops summary under the line. |
| nonstopLabel | React.ReactNode | "Nonstop" | The summary with no stops. |
| stopsLabel | (count: number) => React.ReactNode | (count) => count === 1 ? "1 stop" : ${count} stops | The summary with stops, given their count. |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-path"; the line is "flight-card-path-line". |
FlightCardStop
A connection, its airport code set on the line. Reads "via PHX" to a screen reader.
| Prop | Type | Default | Description |
|---|---|---|---|
| code* | React.ReactNode | - | The airport code. |
| layover | React.ReactNode | - | Time on the ground. With a single stop it joins the summary. |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-stop". |
FlightCardPrice
The fare, 18px semibold in tabular-nums, with a note under it. Start-aligned in the footer, end-aligned when you place it in the header.
| Prop | Type | Default | Description |
|---|---|---|---|
| note | React.ReactNode | - | What the fare covers ("Round trip per person", "Economy"). |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-price". |
FlightCardDetails
Booking facts as a <dl> under a hairline, spread across one line and wrapping only when they run out of room. Four facts fit a 360px chat column.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-details". |
FlightCardDetail
One fact, a small label over its value.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | React.ReactNode | - | What it is ("Gate"). |
| children* | React.ReactNode | - | The value. |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-detail". |
FlightCardFooter
The last line, a <div>: the price on the start edge, the actions on the end, wrapping when narrow.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-footer". |
FlightCardActions
The buttons, pushed to the end and raised above a selectable card's radio. App UI: size="sm", text only.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-actions". |
FlightCardList
Several results in one frame. The cards inside drop their own frame and part with hairlines. Give it value, defaultValue or onValueChange and it becomes a Radix RadioGroup: each card with a value is one option, picked by click or the arrow keys.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | Controlled: the picked card. |
| defaultValue | string | - | Uncontrolled: the card picked at first. |
| onValueChange | (value: string) => void | - | Called when a card is picked. |
| aria-label | string | - | Names the group ("Flights from LAX to DEN"). |
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-list". |
FlightCardSkeleton
The card's shape while results stream in: the airline, two times and the line between. Hidden from screen readers; joins a list's hairlines.
| Data attribute | Description |
|---|---|
| data-slot | "flight-card-skeleton". |