Skip to main content

Catalog · Content

Timeline

Shows dated events in order, one card at a time on a rail of dates or all at once down a line; without JavaScript it is a list.

Fits best: blog, landing — events or steps in order of time, such as a history or a roadmap

Example

The releases of an app as a timeline, in its horizontal and its vertical layouts.

Horizontal, the default. Pick a date, or use Previous and Next, the arrow keys on the dates, or swipe.

  1. First beta

    Ten teams try the app.

  2. Public launch

    Sign-up opens to everyone.

  3. Offline mode

    Notes sync when the connection returns.

  4. Version 2

    A new editor and shared folders.

With autoplay: it moves on by itself while it is on screen. Pause stops it; it also waits while you point at it or use the keyboard in it.

  1. The idea

    Two people sketch the first screens.

  2. The team

    Five people work on it full time.

  3. The first customers

    Fifty schools use it every week.

With layout="vertical": scroll, and the item in the middle of the screen becomes active.

  1. Version 0.1

    Buttons, forms and tables.

  2. Version 0.2

    Dark mode and a print stylesheet.

  3. Version 0.3

    Right-to-left layouts.

  4. Version 1.0

    The API is stable.

Known issues

Three things are not done yet, and none has been tested with a screen reader.

  • The Previous and Next buttons are named only "Previous" and "Next". The name does not say which date they lead to.
  • Whether a screen reader announces the card that appears when you change the item has not been tested.
  • The timeline has not been tested with any screen reader: no result is claimed for NVDA, JAWS, VoiceOver, TalkBack or Orca.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/timeline.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/timeline.min.css" integrity="sha384-WQ94ImVKUsNsMfgRFc24RdFDrzH/9Dc6HCz4FWlUsAcQHX63oP72OzxMchEcBH5g" crossorigin="anonymous">

An ordered list inside <luz-timeline>. Give the element role="group" and a name with aria-label, so the name is read without JavaScript too.

HTML
<luz-timeline role="group" aria-label="Releases of the app">
  <ol class="luz-timeline">
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2025-03">March 2025</time>
      <p class="luz-timeline__title">First beta</p>
      <p class="luz-timeline__text">Ten teams try the app.</p>
    </li>
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2025-06">June 2025</time>
      <p class="luz-timeline__title">Public launch</p>
      <p class="luz-timeline__text">Sign-up opens to everyone.</p>
    </li>
  </ol>
</luz-timeline>

With autoplay, every four seconds:

HTML
<luz-timeline role="group" aria-label="Milestones" autoplay interval="4000">
  <ol class="luz-timeline">
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2023">2023</time>
      <p class="luz-timeline__title">The idea</p>
      <p class="luz-timeline__text">Two people sketch the first screens.</p>
    </li>
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2024">2024</time>
      <p class="luz-timeline__title">The team</p>
      <p class="luz-timeline__text">Five people work on it full time.</p>
    </li>
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2025">2025</time>
      <p class="luz-timeline__title">The first customers</p>
      <p class="luz-timeline__text">Fifty schools use it every week.</p>
    </li>
  </ol>
</luz-timeline>

The vertical layout:

HTML
<luz-timeline role="group" layout="vertical" aria-label="Versions of the library">
  <ol class="luz-timeline">
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2024-02">February 2024</time>
      <h3 class="luz-timeline__title">Version 0.1</h3>
      <p class="luz-timeline__text">Buttons, forms and tables.</p>
    </li>
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2024-09">September 2024</time>
      <h3 class="luz-timeline__title">Version 0.2</h3>
      <p class="luz-timeline__text">Dark mode and a print stylesheet.</p>
    </li>
    <li class="luz-timeline__item">
      <time class="luz-timeline__date" datetime="2025-05">May 2025</time>
      <h3 class="luz-timeline__title">Version 0.3</h3>
      <p class="luz-timeline__text">Right-to-left layouts.</p>
    </li>
  </ol>
</luz-timeline>

For a timeline added to the page after it has loaded, import the module once. It defines the element, and there is no init to call:

JS
import "@luz-ds/catalog/timeline";

Variants and options

The options are attributes of <luz-timeline>. Each item has a date, a title and a text.

Attribute or classEffect
aria-label
The name of the timeline. Write it with role="group": a name on an element without a role is not read. If you leave the role out, the script adds it
layout="vertical"
Every item stays visible down a line. The item in the middle of the screen is active, the ones before it are past
autoplay
Opts in to moving on by itself, with a visible Pause and Play button. Horizontal only; off unless you write it
interval
Milliseconds between items with autoplay. The default is 5000; a value under 2000 is raised to 2000
data-luz-label-previous, -next, -pause, -play
The names of the buttons, when LUZ does not ship the language of the page or you want other words
--luz-timeline-paper
The colour behind the timeline, which the hump on the rail and the empty dots are painted with. The default is --luz-surface-page; on a card, set it on the element to the card's surface: style="--luz-timeline-paper: var(--luz-surface)"
.luz-timeline__date
A <time>. Its text is the label of the date button on the rail, so give each item a different one. Without it the button shows the item's number
.luz-timeline__title
A <p>, or a heading of the level your page needs: the class stays the same
.luz-timeline__text
One or two sentences; a link is allowed
data-luz-state
Written by the script on the items of a vertical timeline: active or past. Do not set it yourself

How it works

A custom element around a native ordered list, with styles from components/timeline.css and a script, timeline.js. catalog.js loads the script only on a page that has a <luz-timeline>.

The element looks for the first .luz-timeline inside it; with none, it does nothing.

  • Horizontal. The script builds, with DOM methods only, Previous and Next at the sides of the card and a rail of dates under it: one button per item, with the text of its date. The cards share one grid cell, so the band is as tall as the longest card and nothing jumps when you change the item. The cards that are not current are inert and hidden. In the current card the date is hidden from sight, because the rail shows it.
  • Changing the item. Click a date, use Previous and Next, the arrow keys, Home and End on the rail, or swipe more than 40 px sideways with a finger or the mouse. Toward the start of the line is the next item. Past either end it wraps round. A swipe that ends on a link is not a click on it.
  • The rail. The current date is scrolled to the middle, with a smooth scroll only if the visitor has not asked for reduced motion. A keyboard move keeps the focus on the new current date, and so does a change that hides the card the focus was in.
  • Vertical. The script marks the item crossing the middle of the screen data-luz-state="active" and the ones before it past. The line behind a past item takes the brand colour, and the active dot is filled and larger. It runs with reduced motion too, without the transition.
  • Autoplay. With autoplay, the item changes every interval milliseconds, in a loop. The Pause button under the rail stops it; it then reads Play and starts it again. The timeline also waits, without a change to the button, while the pointer moves over it (it goes on after 4 seconds still, or when the pointer leaves), while the keyboard focus is inside it, while less than half of it is on screen and while the tab is hidden. With reduced motion it never starts and the button is not shown.
  • Text. "Previous", "Next", "Pause" and "Play" are in the language of the page when LUZ ships it (English, Spanish), chosen by the nearest lang, or yours through data-luz-label-*. They travel with the script, so @luz-ds/catalog/timeline works on its own. A language you add with registerLocale() does not reach them: call initTimeline(document, t) with the t of core. See the JavaScript modules.

Without JavaScript

An ordered list, with every item visible: a line down the inline-start side with a dot at each item, then the date, the title and the text. The list looks the same before the script has defined the element, so nothing jumps when it does.

Autoplay, the rail, Previous and Next, the swipe and the active mark of the vertical layout need JavaScript.

Keyboard and ARIA

Tab reaches, in this order: Previous, the links in the current card, Next, the current date on the rail and the Pause or Play button. The other dates are not in the Tab order: the arrow keys move between them.

  • Left and Right on the rail move to the previous and the next date, and the focus goes with them. In right-to-left text they swap. Past either end it wraps.
  • Home and End go to the first and the last date.
  • Enter or Space on a date, Previous, Next or Pause presses it: they are native buttons.
  • The element has role="group" and your aria-label. The current date has aria-current="true"; only that date is tabbable (a roving tabindex).
  • The cards are one live region. It is aria-live="polite" when the visitor changes the item, and off while autoplay runs, so a card that changes by itself is not announced.
  • The Pause or Play button's name is its action. The arrows' names are "Previous" and "Next": see the known issues.
  • The cards that are not current are inert: they take no focus and are not read. If the focus was in the card that goes, it moves to the new current date.
  • Each card has aria-posinset and aria-setsize, so a screen reader can say "2 of 4" when only one card is shown.

Contrast and focus are in the Design tab.

Design

When to use

Use a timeline for a short run of events with dates, where the order is the point: releases, milestones, a company's history.

  • Horizontal, when each event needs a card and the reader goes through them one by one.
  • Vertical, when the reader should see every event as they scroll.
  • Four to ten items. Beyond that the rail needs a lot of scrolling.

When not to use

Do not use a timeline for steps the reader has to follow in order, or for events without dates.

  • For a process, use a numbered list.
  • To compare events across attributes, use a table.
  • When the reader needs to see every item at once, use the vertical layout: the horizontal one shows a single card.
  • Do not turn on autoplay for content the reader must not miss. Nobody has to wait for the next card.

Do and don't

Do A title that says what happened, with the date in one format.
Don't A vague title and dates in different formats: the reader has to open the card to learn what happened, and cannot compare the dates.
Do A <time> with a different text in each item: it is the name of the button on the rail.
Don't Leave out the date: the rail then has buttons named "1" and "2", and the reader cannot tell them apart.

Writing the content

Put the date first, then what happened, in few words.

  • The date: the same format in every item, and a different text in each. It is the name of a button.
  • The title: two to five words, sentence case, no full stop. Say the event: "Public launch", not "News".
  • The text: one sentence, a few lines at most. In the horizontal layout the band is as tall as the longest card, so keep the cards about the same length.
  • Use the datetime attribute so the date can be read by machines, such as 2025-06.

Contrast and focus

Text is secondary or primary text on the page or on the card. The marks (line, dots, hump, arrows) are borders and strokes.

PartColour on backgroundLightDarkHigh contrastNeeds
Date and text in the list, dates on the rail
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Title in the list, date on the rail on hover
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Title in a card, Previous and Next arrows
--luz-text-primary on --luz-surface
16.5:1
14.0:1
21.0:1
4.5:1
Text in a card
--luz-text-secondary on --luz-surface
6.4:1
6.9:1
19.3:1
4.5:1
Current date, Pause and Play
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
4.5:1
Border of the arrows, line and dots
--luz-outline on --luz-surface-page
4.3:1
3.6:1
21.0:1
3:1
Past line, active dot and hump
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
3:1
Focus ring
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: a 2 px --luz-focus-ring outline, 2 px outside, on the dates and on Previous and Next. Pause and Play is a ghost button and has the button's own focus ring.
  • Not colour alone: the current date is also larger and heavier, and has aria-current. In the vertical layout the active dot is filled and larger, besides the brand colour of the past line.
  • Forced colors: the marks are borders and the hump takes the system text colour, so the line, the dots and the current state stay visible.
  • Target: Previous and Next are 44 px round buttons; each date is a button the width of its slot, 7 rem (10 rem in a band 600 px wide or more).

Tokens it uses

The 26 tokens that components/timeline.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-control-lg, --luz-space-2xs, --luz-space-3xs, --luz-space-lg, --luz-space-md, --luz-space-sm, --luz-space-xl, --luz-space-xs
Motion
--luz-duration-base, --luz-duration-fast
Typography
--luz-font-heading, --luz-font-sans, --luz-text-lg, --luz-text-md, --luz-text-sm, --luz-text-xl
Radius
--luz-radius-full, --luz-radius-md

Keyboard and ARIA are in the Code tab.