JourneyCard

PreviousNext

Displays a round-trip flight itinerary as two expandable leg summaries inside a single bordered card. Each leg shows departure/arrival times, a visual timeline, operator details, cabin characteristics, and an optional per-segment itinerary accordion.

Outbound
Transavia logo
10 Apr. 202611:55
9h30mn
11 Apr. 2026+1 day
(local time)15:25
Paris-OrlyTerminal 2
Direct
Punta CanaTerminal 2E
Economy class, (x1) checked luggage per person — Boeing 777-300e
A total of 750 kg CO₂ emitted.Learn more
LX 633 Transavia
09:45
Arrival: 11 Apr. 202611:05
Paris-Roissy CDGTerminal 2
ZurichTerminal 2E
Flexible flight option • Cancellation fees limited to €300 • Baggage included
Inbound
Transavia logo
16 Apr. 202617:50
8h40mn
17 Apr. 2026+1 day
(local time)08:20
Punta CanaTerminal 2E
1 Stop over
Paris-OrlyTerminal 2
Economy class, (x1) checked luggage per person — Boeing 777-300e
A total of 750 kg CO₂ emitted.Learn more
LX 630 Transavia
09:45
Arrival: 16 Apr. 202611:05
Punta CanaTerminal 2E
ZurichTerminal 2E
1h10min : Airport connection
LX 654 Transavia
12:30
Arrival: 17 Apr. 202608:20
ZurichTerminal 2E
Paris-OrlyTerminal 2
Flexible flight option • Cancellation fees limited to €300 • Baggage included
'use client';

import { JourneyCard } from '@/ui/journey-card/JourneyCard';
import type { JourneyDetailsProps } from '@/ui/journey-card/JourneyDetails';

const outbound: JourneyDetailsProps = {
  direction: 'Outbound',
  directionType: 'Outbound',
  departureDate: '10 Apr. 2026',
  departureTime: '11:55',
  arrivalDate: '11 Apr. 2026',
  arrivalTime: '15:25',
  duration: '9h30mn',
  departureAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
  arrivalAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
  stopLabel: 'Direct',
  airlineLogoSrc: 'https://dummyimage.com/100x28/ccc/555.png&text=Airline',
  airlineLogoAlt: 'Transavia logo',
  operatedBy: 'Transavia',
  companyName: 'Operated by Hop',
  characteristics: 'Economy class, (x1) checked luggage per person — Boeing 777-300e',
  carbonEmission: 'A total of 750 kg CO₂ emitted.',
  carbonEmissionLearnMoreUrl: '#',
  carbonEmissionLearnMoreLabel: 'Learn more',
  plusDays: 1,
  plusDaysLabel: 'day',
  localTimeLabel: '(local time)',
  itinerary: {
    segments: [
      {
        flightNumber: 'LX 633 Transavia',
        departureTime: '09:45',
        arrivalTime: '11:05',
        departureAirport: { name: 'Paris-Roissy CDG', terminal: 'Terminal 2' },
        arrivalAirport: { name: 'Zurich', terminal: 'Terminal 2E' },
        arrivalDate: 'Arrival: 11 Apr. 2026',
      },
    ],
    flexibleOption:
      'Flexible flight option\n• Cancellation fees limited to €300\n• Baggage included',
  },
};

const inbound: JourneyDetailsProps = {
  direction: 'Inbound',
  directionType: 'Inbound',
  departureDate: '16 Apr. 2026',
  departureTime: '17:50',
  arrivalDate: '17 Apr. 2026',
  arrivalTime: '08:20',
  duration: '8h40mn',
  departureAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
  arrivalAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
  stopLabel: '1 Stop over',
  nbStopovers: 1,
  airlineLogoSrc: 'https://dummyimage.com/100x28/ccc/555.png&text=Airline',
  airlineLogoAlt: 'Transavia logo',
  operatedBy: 'Transavia',
  companyName: 'Operated by Hop',
  characteristics: 'Economy class, (x1) checked luggage per person — Boeing 777-300e',
  carbonEmission: 'A total of 750 kg CO₂ emitted.',
  carbonEmissionLearnMoreUrl: '#',
  carbonEmissionLearnMoreLabel: 'Learn more',
  plusDays: 1,
  plusDaysLabel: 'day',
  localTimeLabel: '(local time)',
  itinerary: {
    segments: [
      {
        flightNumber: 'LX 630 Transavia',
        departureTime: '09:45',
        arrivalTime: '11:05',
        departureAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
        arrivalAirport: { name: 'Zurich', terminal: 'Terminal 2E' },
        arrivalDate: 'Arrival: 16 Apr. 2026',
      },
      {
        flightNumber: 'LX 654 Transavia',
        departureTime: '12:30',
        arrivalTime: '08:20',
        departureAirport: { name: 'Zurich', terminal: 'Terminal 2E' },
        arrivalAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
        arrivalDate: 'Arrival: 17 Apr. 2026',
      },
    ],
    connectionTime: '1h10min : Airport connection',
    flexibleOption:
      'Flexible flight option\n• Cancellation fees limited to €300\n• Baggage included',
  },
};

export function JourneyCardDemo() {
  return (
    <div className="w-full">
      <JourneyCard flight1Journey={outbound} flight2Journey={inbound} />
    </div>
  );
}

Installation

npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/journey-card.json

Usage

import { JourneyCard } from '@/ui/journey-card/JourneyCard';
import type { JourneyDetailsProps } from '@/ui/journey-card/JourneyDetails';
 
const outbound: JourneyDetailsProps = {
  direction: 'Outbound',
  directionType: 'Outbound',
  departureDate: '10 Apr. 2026',
  departureTime: '11:55',
  arrivalDate: '11 Apr. 2026',
  arrivalTime: '15:25',
  duration: '9h30mn',
  departureAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
  arrivalAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
  stopLabel: 'Direct',
  airlineLogoSrc: '/images/transavia.png',
  airlineLogoAlt: 'Transavia logo',
  operatedBy: 'Transavia',
  companyName: 'Operated by Hop',
  characteristics: 'Economy class, (x1) checked luggage — Boeing 777-300e',
  carbonEmission: 'A total of 750 kg CO₂ emitted.',
  carbonEmissionLearnMoreUrl: 'https://example.com/eco',
  carbonEmissionLearnMoreLabel: 'Learn more',
  plusDays: 1,
  plusDaysLabel: 'day',
  localTimeLabel: '(local time)',
};
 
const inbound: JourneyDetailsProps = { ...outbound, direction: 'Inbound', directionType: 'Inbound' };
 
export function Component() {
  return <JourneyCard flight1Journey={outbound} flight2Journey={inbound} />;
}

Architecture

JourneyCard is composed of four nested sub-components that can also be used independently:

JourneyCard
└── JourneyDetails          ×2 (one per leg)
    ├── FlightTimeline       (visual departure → arrival line with stopover dots)
    ├── Tag                  (trident-ui Tag, for +N days badge)
    ├── ElasticHeight        (accordion animation wrapper)
    └── ItineraryDetails     (optional, inside accordion)
        └── FlightSegmentCard ×N (one per flight segment)
            └── FlightTimeline

All sub-components accept standard div props and forward them to their root element, so you can pass className, data-testid, aria-* attributes freely.

API Reference

JourneyCard

PropTypeDefaultDescription
flight1JourneyJourneyDetailsProps—First flight leg (typically outbound)
flight2JourneyJourneyDetailsProps—Second flight leg (typically inbound)
compactbooleanfalseWhen true, the two legs are placed side-by-side with a vertical divider instead of stacked
classNamestringundefinedExtra classes for the outer wrapper div
...propsComponentPropsWithoutRef<'div'>—All standard div attributes are forwarded

JourneyDetails

Renders a single flight leg. Clicking the operator row toggles an accordion that reveals cabin characteristics, carbon emission info, and the optional itinerary.

PropTypeDefaultDescription
directionstring—Human-readable leg label, e.g. "Outbound" or "Aller"
directionType'Inbound' | 'Outbound'undefinedControls which plane icon is shown; falls back to direction
departureDatestring—Formatted departure date, e.g. "10 Apr. 2026"
departureTimestring—Departure time string, e.g. "11:55"
arrivalDatestring—Formatted arrival date
arrivalTimestring—Arrival time string
durationstring—Total flight duration, e.g. "9h30mn"
departureAirportAirportInfo—{ name: string; terminal?: string }
arrivalAirportAirportInfo—{ name: string; terminal?: string }
stopLabelstring—Connection label shown on the timeline, e.g. "Direct" or "1 Stop over"
nbStopoversnumber0Number of black stopover dots rendered on the FlightTimeline
airlineLogoSrcstring—URL for the airline logo image
airlineLogoAltstring—Accessible alt text for the airline logo
operatedBystring—Primary operator name
companyNamestringundefinedSecondary line shown below operatedBy, e.g. "Operated by Hop"
characteristicsstring—Cabin / luggage summary (shown inside accordion)
carbonEmissionstring—Carbon footprint description text (shown inside accordion)
carbonEmissionLearnMoreUrlstringundefinedIf provided, renders a learn-more link below the emission text
carbonEmissionLearnMoreLabelstring—Label for the learn-more link
plusDaysnumberundefinedWhen > 0, renders a +N day(s) badge next to the arrival date
plusDaysLabelstring—Text appended to the plusDays value, e.g. "day"
localTimeLabelstring—Qualifier shown near the arrival time, e.g. "(local time)"
itineraryItineraryDetailsPropsundefinedIf provided, renders per-segment flight cards inside the accordion
classNamestringundefinedExtra classes for the outer wrapper

ItineraryDetails

Renders a list of FlightSegmentCard components inside a sand background panel, with optional connection time and flexible option text.

PropTypeDefaultDescription
segmentsFlightSegmentCardProps[]—One entry per flight segment
connectionTimestringundefinedConnection time shown between consecutive segments
flexibleOptionstringundefinedFree-text flexible fare info shown at the bottom
classNamestringundefinedExtra classes for the outer wrapper

FlightSegmentCard

Renders a single flight segment (flight number, times, and airport names).

PropTypeDefaultDescription
flightNumberstring—Flight identifier, e.g. "LX 633 Transavia"
departureTimestring—Segment departure time
arrivalTimestring—Segment arrival time
departureAirportAirportInfo—{ name: string; terminal?: string }
arrivalAirportAirportInfo—{ name: string; terminal?: string }
arrivalDatestringundefinedOptional arrival date label rendered above the arrival time
classNamestringundefinedExtra classes for the outer wrapper

FlightTimeline

A thin horizontal line with circular endpoint markers. Renders filled black dots for stopovers.

PropTypeDefaultDescription
nbStopoversnumber0Number of filled stopover dots in the middle
classNamestringundefinedExtra classes for the outer wrapper

AirportInfo

interface AirportInfo {
  name: string;
  terminal?: string;
}

Examples

Compact layout

Use compact on wider viewports to display both legs side-by-side. The horizontal separator becomes a vertical one and each leg takes equal width.

Outbound
Transavia logo
10 Apr. 202611:55
9h30mn
11 Apr. 2026
(local time)15:25
Paris-OrlyTerminal 2
Direct
Punta CanaTerminal 2E
Economy class, (x1) checked luggage per person — Boeing 777-300e
A total of 750 kg CO₂ emitted.
Inbound
Transavia logo
16 Apr. 202617:50
8h40mn
17 Apr. 2026+1 day
(local time)08:20
Punta CanaTerminal 2E
1 Stop over
Paris-OrlyTerminal 2
Economy class, (x1) checked luggage per person — Boeing 777-300e
A total of 750 kg CO₂ emitted.
'use client';

import { JourneyCard } from '@/ui/journey-card/JourneyCard';
import type { JourneyDetailsProps } from '@/ui/journey-card/JourneyDetails';

const outbound: JourneyDetailsProps = {
  direction: 'Outbound',
  directionType: 'Outbound',
  departureDate: '10 Apr. 2026',
  departureTime: '11:55',
  arrivalDate: '11 Apr. 2026',
  arrivalTime: '15:25',
  duration: '9h30mn',
  departureAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
  arrivalAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
  stopLabel: 'Direct',
  airlineLogoSrc: 'https://dummyimage.com/100x28/ccc/555.png&text=Airline',
  airlineLogoAlt: 'Transavia logo',
  operatedBy: 'Transavia',
  characteristics: 'Economy class, (x1) checked luggage per person — Boeing 777-300e',
  carbonEmission: 'A total of 750 kg CO₂ emitted.',
  carbonEmissionLearnMoreLabel: 'Learn more',
  plusDaysLabel: 'day',
  localTimeLabel: '(local time)',
};

const inbound: JourneyDetailsProps = {
  direction: 'Inbound',
  directionType: 'Inbound',
  departureDate: '16 Apr. 2026',
  departureTime: '17:50',
  arrivalDate: '17 Apr. 2026',
  arrivalTime: '08:20',
  duration: '8h40mn',
  departureAirport: { name: 'Punta Cana', terminal: 'Terminal 2E' },
  arrivalAirport: { name: 'Paris-Orly', terminal: 'Terminal 2' },
  stopLabel: '1 Stop over',
  nbStopovers: 1,
  airlineLogoSrc: 'https://dummyimage.com/100x28/ccc/555.png&text=Airline',
  airlineLogoAlt: 'Transavia logo',
  operatedBy: 'Transavia',
  characteristics: 'Economy class, (x1) checked luggage per person — Boeing 777-300e',
  carbonEmission: 'A total of 750 kg CO₂ emitted.',
  carbonEmissionLearnMoreLabel: 'Learn more',
  plusDays: 1,
  plusDaysLabel: 'day',
  localTimeLabel: '(local time)',
};

export function JourneyCardCompactDemo() {
  return (
    <div className="w-full">
      <JourneyCard compact flight1Journey={outbound} flight2Journey={inbound} />
    </div>
  );
}

Behaviour

Compact vs default

By default the two JourneyDetails legs stack vertically, separated by a full-width horizontal line. When compact is true they sit in a flex-row, each taking flex-1, separated by a 1 px vertical line. The accordion inside each leg still works independently in compact mode.

Accordion expand / collapse

Each JourneyDetails leg has an operator row that acts as a <button>. Clicking it toggles the ElasticHeight accordion that reveals:

  • Cabin characteristics (luggage allowance, aircraft type, …)
  • Carbon emission summary and optional learn-more link
  • ItineraryDetails (per-segment cards) — only rendered when itinerary prop is provided

The button carries aria-expanded so assistive technologies can announce the accordion state.

+N days badge

When plusDays > 0, a trident-ui Tag is rendered inline next to the arrival date to signal an overnight or multi-day journey.

Stopover dots

nbStopovers controls how many filled dots appear inside FlightTimeline. Both the top-level JourneyDetails timeline and each FlightSegmentCard timeline accept this prop — pass it at both levels for visual consistency.

directionType vs direction

direction is the human-readable label rendered as text (can be any locale string). directionType is strictly 'Inbound' | 'Outbound' and controls which plane icon (PlaneInBound or PlaneOutbound) is rendered. When directionType is omitted, direction is used for the icon lookup, so English values work out of the box.

Accessibility

  • The accordion toggle is a native <button> with aria-expanded reflecting the open/closed state.
  • The horizontal separator between the two legs uses role="separator".
  • The airline logo carries a meaningful alt attribute — pass an empty string ("") only if the logo is purely decorative and the operator name is already visible.
  • Carbon emission learn-more links use visible anchor text; avoid using "Click here" as label text.
  • FlightTimeline end-point circles are decorative — they carry no accessible role or text.

Notes

  • All sub-components (FlightTimeline, FlightSegmentCard, ItineraryDetails, JourneyDetails) are exported from their own files and can be used in isolation.
  • JourneyCard always renders exactly two legs. If you need a one-way card, use JourneyDetails directly.
  • The 'use client' directive is only on JourneyDetails (it owns useState). The pure presentational sub-components are server-safe.
  • The carbon emission icon used is HappyToCare from @clubmed/trident-icons, which maps to the green-leaf biodiversity icon from the original design.
  • Flexible option text in ItineraryDetails.flexibleOption supports \n line breaks and is rendered with whitespace-pre-wrap.