- Accordion
- Advanced Toast
- AlertMessage
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- Chapters
- ChatBody
- ChatButton
- ChatHeader
- ChatInput
- ChatMessageActions
- ChatMessageEditor
- ChatMessage
- ChatTypingIndicator
- ChatWindowCsatPrompt
- ChatWindowQuote
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- ContentReview
- Countdown
- CriteriaDates
- CriteriaParticipants
- CriteriaTransports
- Date Field
- Datepicker
- Dropdown
- ElasticHeight
- ExpandableCard
- ExpandableText
- Filter
- FloatingLabelTextField
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- JourneyCard
- LayerHeader
- Layer
- Link
- Loader
- Number Field
- PageNavigation
- Pagination
- Paragraph
- Password
- Phone Field
- Popin
- Portal
- ProductThumbnail
- Promises
- Prose
- Radio Group
- RadioSelect
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- SparkleWrapper
- Spinner
- Switch
- Table
- Tabs
- Tag
- TextField
- Toggle
- Tooltip
- ValidationMessage
'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
| 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
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.
'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
itineraryprop 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>witharia-expandedreflecting the open/closed state. - The horizontal separator between the two legs uses
role="separator". - The airline logo carries a meaningful
altattribute — 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. FlightTimelineend-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. JourneyCardalways renders exactly two legs. If you need a one-way card, useJourneyDetailsdirectly.- The
'use client'directive is only onJourneyDetails(it ownsuseState). The pure presentational sub-components are server-safe. - The carbon emission icon used is
HappyToCarefrom@clubmed/trident-icons, which maps to the green-leaf biodiversity icon from the original design. - Flexible option text in
ItineraryDetails.flexibleOptionsupports\nline breaks and is rendered withwhitespace-pre-wrap.