---
title: JourneyCard
description: 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.
---

```tsx
'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

Using the CLI:

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

Or manually, copy the following code into `/ui/journey-card/JourneyCard.tsx`:

```tsx
import clsx from 'clsx';
import type { ComponentPropsWithoutRef } from 'react';
import { JourneyDetails } from './JourneyDetails';
import type { JourneyDetailsProps } from './JourneyDetails';

export type { JourneyDetailsProps };
export type { AirportInfo, ItineraryDetailsProps } from './ItineraryDetails';
export type { FlightSegmentCardProps } from './FlightSegmentCard';

export interface JourneyCardProps extends ComponentPropsWithoutRef<'div'> {
  /** Outbound or inbound flight leg */
  flight1Journey: JourneyDetailsProps;
  /** Return flight leg */
  flight2Journey: JourneyDetailsProps;
  /**
   * When true, both legs are displayed side-by-side with a vertical divider
   * instead of stacked with a horizontal divider.
   */
  compact?: boolean;
}

export function JourneyCard({
  flight1Journey,
  flight2Journey,
  compact = false,
  className,
  ...props
}: JourneyCardProps) {
  return (
    <div
      data-name="JourneyCard"
      {...props}
      className={clsx('flex h-fit w-full flex-col', className)}
    >
      <div
        className={clsx(
          'flex w-full rounded-32 border border-lightGrey bg-white p-20',
          compact ? 'flex-row gap-0' : 'flex-col gap-12',
        )}
      >
        <JourneyDetails
          {...flight1Journey}
          className={clsx(compact && 'flex-1 pe-12')}
          vertical={compact}
        />
        {!compact && <div role="separator" className={clsx('shrink-0 bg-lightGrey h-px w-full')} />}
        <JourneyDetails
          {...flight2Journey}
          className={clsx(compact && 'flex-1 ps-12')}
          vertical={compact}
        />
      </div>
    </div>
  );
}
```

## Usage

```tsx showLineNumbers
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

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `flight1Journey` | `JourneyDetailsProps` | — | First flight leg (typically outbound) |
| `flight2Journey` | `JourneyDetailsProps` | — | Second flight leg (typically inbound) |
| `compact` | `boolean` | `false` | When `true`, the two legs are placed side-by-side with a vertical divider instead of stacked |
| `className` | `string` | `undefined` | Extra classes for the outer wrapper `div` |
| `...props` | `ComponentPropsWithoutRef<'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.

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `direction` | `string` | — | Human-readable leg label, e.g. `"Outbound"` or `"Aller"` |
| `directionType` | `'Inbound' \| 'Outbound'` | `undefined` | Controls which plane icon is shown; falls back to `direction` |
| `departureDate` | `string` | — | Formatted departure date, e.g. `"10 Apr. 2026"` |
| `departureTime` | `string` | — | Departure time string, e.g. `"11:55"` |
| `arrivalDate` | `string` | — | Formatted arrival date |
| `arrivalTime` | `string` | — | Arrival time string |
| `duration` | `string` | — | Total flight duration, e.g. `"9h30mn"` |
| `departureAirport` | `AirportInfo` | — | `{ name: string; terminal?: string }` |
| `arrivalAirport` | `AirportInfo` | — | `{ name: string; terminal?: string }` |
| `stopLabel` | `string` | — | Connection label shown on the timeline, e.g. `"Direct"` or `"1 Stop over"` |
| `nbStopovers` | `number` | `0` | Number of black stopover dots rendered on the FlightTimeline |
| `airlineLogoSrc` | `string` | — | URL for the airline logo image |
| `airlineLogoAlt` | `string` | — | Accessible alt text for the airline logo |
| `operatedBy` | `string` | — | Primary operator name |
| `companyName` | `string` | `undefined` | Secondary line shown below `operatedBy`, e.g. `"Operated by Hop"` |
| `characteristics` | `string` | — | Cabin / luggage summary (shown inside accordion) |
| `carbonEmission` | `string` | — | Carbon footprint description text (shown inside accordion) |
| `carbonEmissionLearnMoreUrl` | `string` | `undefined` | If provided, renders a learn-more link below the emission text |
| `carbonEmissionLearnMoreLabel` | `string` | — | Label for the learn-more link |
| `plusDays` | `number` | `undefined` | When `> 0`, renders a `+N day(s)` badge next to the arrival date |
| `plusDaysLabel` | `string` | — | Text appended to the `plusDays` value, e.g. `"day"` |
| `localTimeLabel` | `string` | — | Qualifier shown near the arrival time, e.g. `"(local time)"` |
| `itinerary` | `ItineraryDetailsProps` | `undefined` | If provided, renders per-segment flight cards inside the accordion |
| `className` | `string` | `undefined` | Extra 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.

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `segments` | `FlightSegmentCardProps[]` | — | One entry per flight segment |
| `connectionTime` | `string` | `undefined` | Connection time shown between consecutive segments |
| `flexibleOption` | `string` | `undefined` | Free-text flexible fare info shown at the bottom |
| `className` | `string` | `undefined` | Extra classes for the outer wrapper |

---

### FlightSegmentCard

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

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `flightNumber` | `string` | — | Flight identifier, e.g. `"LX 633 Transavia"` |
| `departureTime` | `string` | — | Segment departure time |
| `arrivalTime` | `string` | — | Segment arrival time |
| `departureAirport` | `AirportInfo` | — | `{ name: string; terminal?: string }` |
| `arrivalAirport` | `AirportInfo` | — | `{ name: string; terminal?: string }` |
| `arrivalDate` | `string` | `undefined` | Optional arrival date label rendered above the arrival time |
| `className` | `string` | `undefined` | Extra classes for the outer wrapper |

---

### FlightTimeline

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

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `nbStopovers` | `number` | `0` | Number of filled stopover dots in the middle |
| `className` | `string` | `undefined` | Extra classes for the outer wrapper |

---

### AirportInfo

```ts
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.

```tsx
'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`.
