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.
-
First beta
Ten teams try the app.
-
Public launch
Sign-up opens to everyone.
-
Offline mode
Notes sync when the connection returns.
-
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.
-
The idea
Two people sketch the first screens.
-
The team
Five people work on it full time.
-
The first customers
Fifty schools use it every week.
With layout="vertical": scroll, and the item in the middle of the screen becomes active.
-
Version 0.1
Buttons, forms and tables.
-
Version 0.2
Dark mode and a print stylesheet.
-
Version 0.3
Right-to-left layouts.
-
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
import "@luz-ds/catalog/components/timeline.css";
<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.
<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:
<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:
<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:
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 class | Effect |
|---|---|
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 | 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 | A <p>, or a heading of the level your page needs: the class stays the same |
.luz-timeline | 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
inertand 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 itpast. 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 everyintervalmilliseconds, 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 throughdata-luz-label-*. They travel with the script, so@luz-ds/catalog/timelineworks on its own. A language you add withregisterLocale()does not reach them: callinitTimeline(document, t)with thetof 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 youraria-label. The current date hasaria-current="true"; only that date is tabbable (a rovingtabindex). - The cards are one live region. It is
aria-live="polite"when the visitor changes the item, andoffwhile 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-posinsetandaria-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
<time> with a different text in each item: it is the name of the button on the rail.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
datetimeattribute so the date can be read by machines, such as2025-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.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
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-ringoutline, 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.
- Color
--luz-brand-primary,--luz-divider,--luz-focus-ring,--luz-outline,--luz-surface,--luz-surface-page,--luz-text-primary,--luz-text-secondary- 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.