32 CSS Timelines31 / 32

Pure CSSMIT licensed

Flight Itinerary Timeline — Segments, Layover and Timezone Footnotes

The booking-confirmation pattern: flight legs as airport-code pairs joined by a dotted route with a plane glyph riding it, duration bars proportional to real flight time, and the layover rendered as an honest dashed gap with a 'short connection' warning chip — the domain where a timeline's geometry genuinely helps people not miss planes.

Published Updated

Live Demo
Try it

The code

<section class="tls-31" aria-label="Flight itinerary timeline demo">
  <div class="tls-31__card">
    <header class="tls-31__head">
      <h3 class="tls-31__title">SFO → Rome</h3>
      <p class="tls-31__meta"><time datetime="2026-09-12">Fri, Sep 12 2026</time> · 1 stop · 14h 20m total</p>
    </header>
    <ol class="tls-31__legs" role="list">
      <li class="tls-31__leg" style="--dur:470">
        <div class="tls-31__ap"><b>SFO</b><span>San Francisco</span><time datetime="2026-09-12T14:10-07:00">14:10</time></div>
        <div class="tls-31__route"><span class="tls-31__plane" aria-hidden="true">▸</span><span class="tls-31__dur">7h 50m · VA 204</span></div>
        <div class="tls-31__ap tls-31__ap--arr"><b>JFK</b><span>New York</span><time datetime="2026-09-12T22:00-04:00">22:00</time></div>
      </li>
      <li class="tls-31__stop" data-tight="true">
        <span class="tls-31__stopline" aria-hidden="true"></span>
        <p class="tls-31__stopchip">⏱ 55 min connection — short<span class="tls-31__sr">, tight connection warning</span></p>
        <span class="tls-31__stopline" aria-hidden="true"></span>
      </li>
      <li class="tls-31__leg" style="--dur:335">
        <div class="tls-31__ap"><b>JFK</b><span>New York</span><time datetime="2026-09-12T22:55-04:00">22:55</time></div>
        <div class="tls-31__route"><span class="tls-31__plane" aria-hidden="true">▸</span><span class="tls-31__dur">8h 20m · VA 88</span></div>
        <div class="tls-31__ap tls-31__ap--arr"><b>FCO</b><span>Rome</span><time datetime="2026-09-13T13:15+02:00">13:15<sup>+1</sup></time></div>
      </li>
    </ol>
    <footer class="tls-31__foot"><p><sup>+1</sup> Arrives the next calendar day. All times local.</p></footer>
  </div>
</section>
.tls-31,
.tls-31 *,
.tls-31 *::before,
.tls-31 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-31 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #1d4ed8;
  --tl-line: #e2e8f0;
  --tl-ink: #0f172a;
  --tl-mut: #64748b;
  --tl-warn: #b45309;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f1f5f9;
  padding: 64px 24px;
  display: grid;
  place-items: center;
}

.tls-31__card {
  width: min(600px,100%);
  background: #fff;
  border: 1px solid var(--tl-line);
  border-radius: 18px;
  padding: 26px 28px;
  box-shadow: 0 12px 36px -24px rgb(15 23 42 / .35);
}

.tls-31__head {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--tl-line);
  margin-bottom: 20px;
}

.tls-31__title {
  font-size: clamp(20px,3.5vw,25px);
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: -.02em;
}

.tls-31__meta {
  font-size: 12.5px;
  color: var(--tl-mut);
  margin-top: 4px;
}

.tls-31__legs {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.tls-31__leg {
  display: flex;
  align-items: center;
  gap: 16px;
}

.tls-31__ap {
  flex: none;
  width: 96px;
  display: grid;
  gap: 1px;
}

.tls-31__ap--arr {
  text-align: right;
}

.tls-31__ap b {
  font-size: 22px;
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: .02em;
}

.tls-31__ap span {
  font-size: 11px;
  color: var(--tl-mut);
}

.tls-31__ap time {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--tl-accent);
  font-variant-numeric: tabular-nums;
}

.tls-31__ap sup {
  font-size: 9px;
  color: var(--tl-warn);
  font-weight: 800;
}

.tls-31__route {
  position: relative;
  flex-grow: var(--dur,300);
  border-top: 2px dotted #b6c4d8;
  margin-top: 4px;
  min-width: 80px;
}

.tls-31__plane {
  position: absolute;
  top: -11px;
  left: 50%;
  translate: -50% 0;
  color: var(--tl-accent);
  font-size: 14px;
  background: #fff;
  padding-inline: 5px;
}

.tls-31__dur {
  position: absolute;
  top: 8px;
  left: 50%;
  translate: -50% 0;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--tl-mut);
  white-space: nowrap;
}

.tls-31__stop {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 18px 0 18px 40px;
  gap: 6px;
}

.tls-31__stopline {
  width: 2px;
  height: 16px;
  background: repeating-linear-gradient(180deg,#b6c4d8 0 4px,transparent 4px 8px);
}

.tls-31__stopchip {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--tl-mut);
  background: #f8fafc;
  border: 1px solid var(--tl-line);
  border-radius: 999px;
  padding: 5px 12px;
}

.tls-31__stop[data-tight="true"] .tls-31__stopchip {
  color: var(--tl-warn);
  background: #fffbeb;
  border-color: #fde68a;
}

.tls-31__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.tls-31__foot {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid var(--tl-line);
}

.tls-31__foot p {
  font-size: 11px;
  color: var(--tl-mut);
}

.tls-31__foot sup {
  color: var(--tl-warn);
  font-weight: 800;
}

@media (max-width: 520px) {
  .tls-31__leg {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
  }

  .tls-31__ap,
    .tls-31__ap--arr {
    width: auto;
    text-align: left;
    display: flex;
    align-items: baseline;
    gap: 10px;
  }

  .tls-31__route {
    min-height: 34px;
    flex-grow: 0;
  }

  .tls-31__stop {
    padding-left: 0;
  }
}
/* No JavaScript — route widths grow proportionally to flight minutes via flex-grow:var(--dur); the layover is a first-class list item whose data-tight attribute flips the warning chip. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Timeline from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Flight Itinerary Timeline — Segments, Layover and Timezone Footnotes
Source: https://codefronts.com/layouts/css-timelines/film-strip-portfolio/

The booking-confirmation pattern: flight legs as airport-code pairs joined by a dotted route with a plane glyph riding it, duration bars proportional to real flight time, and the layover rendered as an honest dashed gap with a 'short connection' warning chip — the domain where a timeline's geometry genuinely helps people not miss planes.
## HTML
```html
<section class="tls-31" aria-label="Flight itinerary timeline demo">
  <div class="tls-31__card">
    <header class="tls-31__head">
      <h3 class="tls-31__title">SFO → Rome</h3>
      <p class="tls-31__meta"><time datetime="2026-09-12">Fri, Sep 12 2026</time> · 1 stop · 14h 20m total</p>
    </header>
    <ol class="tls-31__legs" role="list">
      <li class="tls-31__leg" style="--dur:470">
        <div class="tls-31__ap"><b>SFO</b><span>San Francisco</span><time datetime="2026-09-12T14:10-07:00">14:10</time></div>
        <div class="tls-31__route"><span class="tls-31__plane" aria-hidden="true">▸</span><span class="tls-31__dur">7h 50m · VA 204</span></div>
        <div class="tls-31__ap tls-31__ap--arr"><b>JFK</b><span>New York</span><time datetime="2026-09-12T22:00-04:00">22:00</time></div>
      </li>
      <li class="tls-31__stop" data-tight="true">
        <span class="tls-31__stopline" aria-hidden="true"></span>
        <p class="tls-31__stopchip">⏱ 55 min connection — short<span class="tls-31__sr">, tight connection warning</span></p>
        <span class="tls-31__stopline" aria-hidden="true"></span>
      </li>
      <li class="tls-31__leg" style="--dur:335">
        <div class="tls-31__ap"><b>JFK</b><span>New York</span><time datetime="2026-09-12T22:55-04:00">22:55</time></div>
        <div class="tls-31__route"><span class="tls-31__plane" aria-hidden="true">▸</span><span class="tls-31__dur">8h 20m · VA 88</span></div>
        <div class="tls-31__ap tls-31__ap--arr"><b>FCO</b><span>Rome</span><time datetime="2026-09-13T13:15+02:00">13:15<sup>+1</sup></time></div>
      </li>
    </ol>
    <footer class="tls-31__foot"><p><sup>+1</sup> Arrives the next calendar day. All times local.</p></footer>
  </div>
</section>
```
## CSS
```css
.tls-31,
.tls-31 *,
.tls-31 *::before,
.tls-31 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-31 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #1d4ed8;
  --tl-line: #e2e8f0;
  --tl-ink: #0f172a;
  --tl-mut: #64748b;
  --tl-warn: #b45309;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f1f5f9;
  padding: 64px 24px;
  display: grid;
  place-items: center;
}

.tls-31__card {
  width: min(600px,100%);
  background: #fff;
  border: 1px solid var(--tl-line);
  border-radius: 18px;
  padding: 26px 28px;
  box-shadow: 0 12px 36px -24px rgb(15 23 42 / .35);
}

.tls-31__head {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--tl-line);
  margin-bottom: 20px;
}

.tls-31__title {
  font-size: clamp(20px,3.5vw,25px);
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: -.02em;
}

.tls-31__meta {
  font-size: 12.5px;
  color: var(--tl-mut);
  margin-top: 4px;
}

.tls-31__legs {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.tls-31__leg {
  display: flex;
  align-items: center;
  gap: 16px;
}

.tls-31__ap {
  flex: none;
  width: 96px;
  display: grid;
  gap: 1px;
}

.tls-31__ap--arr {
  text-align: right;
}

.tls-31__ap b {
  font-size: 22px;
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: .02em;
}

.tls-31__ap span {
  font-size: 11px;
  color: var(--tl-mut);
}

.tls-31__ap time {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--tl-accent);
  font-variant-numeric: tabular-nums;
}

.tls-31__ap sup {
  font-size: 9px;
  color: var(--tl-warn);
  font-weight: 800;
}

.tls-31__route {
  position: relative;
  flex-grow: var(--dur,300);
  border-top: 2px dotted #b6c4d8;
  margin-top: 4px;
  min-width: 80px;
}

.tls-31__plane {
  position: absolute;
  top: -11px;
  left: 50%;
  translate: -50% 0;
  color: var(--tl-accent);
  font-size: 14px;
  background: #fff;
  padding-inline: 5px;
}

.tls-31__dur {
  position: absolute;
  top: 8px;
  left: 50%;
  translate: -50% 0;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--tl-mut);
  white-space: nowrap;
}

.tls-31__stop {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 18px 0 18px 40px;
  gap: 6px;
}

.tls-31__stopline {
  width: 2px;
  height: 16px;
  background: repeating-linear-gradient(180deg,#b6c4d8 0 4px,transparent 4px 8px);
}

.tls-31__stopchip {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--tl-mut);
  background: #f8fafc;
  border: 1px solid var(--tl-line);
  border-radius: 999px;
  padding: 5px 12px;
}

.tls-31__stop[data-tight="true"] .tls-31__stopchip {
  color: var(--tl-warn);
  background: #fffbeb;
  border-color: #fde68a;
}

.tls-31__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.tls-31__foot {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid var(--tl-line);
}

.tls-31__foot p {
  font-size: 11px;
  color: var(--tl-mut);
}

.tls-31__foot sup {
  color: var(--tl-warn);
  font-weight: 800;
}

@media (max-width: 520px) {
  .tls-31__leg {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
  }

  .tls-31__ap,
    .tls-31__ap--arr {
    width: auto;
    text-align: left;
    display: flex;
    align-items: baseline;
    gap: 10px;
  }

  .tls-31__route {
    min-height: 34px;
    flex-grow: 0;
  }

  .tls-31__stop {
    padding-left: 0;
  }
}
```

## JavaScript
```js
/* No JavaScript — route widths grow proportionally to flight minutes via flex-grow:var(--dur); the layover is a first-class list item whose data-tight attribute flips the warning chip. */
```

How this works

Each leg is a grid row: departure block (airport code + city + <time>), a route track, and an arrival block. The route is a border-top: 2px dotted line with a ▸ glyph positioned mid-track and the flight duration labeled beneath it. Leg route widths are proportional to real durations via flex-grow set from a --dur custom property (minutes) — the 7h50 leg visibly dwarfs the 1h10 hop, which is information no table conveys at a glance.

The layover is its own row rendered in a different grammar — a vertical dashed connector between legs with a clock chip stating '55 min connection' and a data-tight attribute that flips the chip to warning styling below a threshold you set in the data. Semantically it's an <li> like the legs, because a layover IS an event, not decoration.

Times print in local time with UTC offsets as small footnote markers (+1 superscript on the overnight arrival, expanded in a footer note) — the single hardest-earned convention in itinerary design, done in text so screen readers get it too. Everything themes from tokens; the whole card prints beautifully because it's borders and text.

Techniques used: duration-proportional route tracks via flex-grow from a --dur token · dotted-border routes with positioned plane glyph · layover as a first-class list item with data-tight warning state · superscript day-offset markers with footnote expansion · airport-code typographic hierarchy (code big, city small) · time[datetime] with full ISO offsets · print-clean border/text construction

Make it yours

  • Add a leg: one li with --dur set to the flight minutes — track widths rebalance automatically.
  • Tight-connection threshold: you decide what gets data-tight in the markup/data; the CSS just styles it.
  • Airline branding: the accent token + the chip on each leg (carrier + flight number).
  • One-way vs return: duplicate the card; don't merge outbound and return into one rail — travelers read them as separate documents.

Gotchas — read before shipping

  • Always render LOCAL times with explicit day-offset markers — the +1 superscript on overnight arrivals prevents the most expensive misreading in travel UI.
  • The datetime attribute should carry the full ISO offset (2026-09-12T22:45+02:00) even though the visible text is short — machines get the truth, humans get the summary.
  • Don't scale route widths to GEOGRAPHIC distance — passengers care about time in seat, not kilometers; --dur is minutes on purpose.
  • Keep the layover visually DIFFERENT from flights (vertical, dashed, chip) — rendering it as another horizontal segment makes it read as a third flight.

Browser support

ChromeSafariFirefoxEdge
allallallall

Flexbox, borders and attribute selectors — nothing gated; the card is also print-safe by construction.

Techniques used in this demo

Search CodeFronts

Loading…