Timeline
A chronology: an ordered list whose children are entries, group headings and a now marker. The rail is not one line down the middle — each entry paints the half above its node and the half below, which is how a heading or a marker breaks the line by painting neither. Every layout is the same DOM on a different grid, and the scroll-driven reveal sits behind a support query with a complete static fallback underneath it.
- Stable
- React 19
- Zero JS
- 4 layouts, 4 rails
- Scroll-driven CSS
-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
-
Out for delivery
Three stops ahead of yours.
-
Estimated arrival
Not yet left the last stop.
Two done, one current with a pulsing node, one upcoming on a dotted rail.
-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
-
Out for delivery
Three stops ahead of yours.
Code
<Timeline aria-label="Delivery status"><TimelineItem color="success" state="done" time="09:41" dateTime="2026-08-21T09:41" title="Order confirmed" description="Card ending 4417 accepted."/><TimelineItem color="success" state="done" time="11:20" title="Packed" … /><TimelineItem color="info" state="current" time="14:05" title="Out for delivery" … /></Timeline><ol class="wayra-timeline" aria-label="Usage timeline" data-size="md" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T11:20">11:20</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Packed</span> <p class="wayra-timeline-desc">Boxed at the Ñuñoa depot.</p> </div> </li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <time class="wayra-timeline-time" datetime="2026-08-21T14:05">14:05</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Out for delivery</span> <p class="wayra-timeline-desc">Three stops ahead of yours.</p> </div> </li></ol>The root is an <ol>, not a <ul>: the order of the list is the order of the
events, and an ordered list is what says so. Every slot is optional — an entry
with nothing but a title still draws its node and its rail. dateTime is what
turns the visible stamp into a real <time datetime>; without it the stamp is a
plain <span>.
The three states
Section titled “The three states”state fills the rail and shapes the node. It is a look and nothing else: a
screen reader hears the entry’s text and no more, so words like “delivered” or
“pending” have to be in that text as well. The library injects no such label —
the same rule Radio follows for its group error.
state |
The rail | The node | The text |
|---|---|---|---|
| (none) | Neutral hairline | Solid dot in the role | Ink |
done |
Both halves in the role | Solid dot in the role | Ink |
current |
The half above the node in the role | Solid, with a pulsing ring | Title takes the accent |
upcoming |
Dotted | Hollow, ringed | Title and description muted |
current is the only state with an animation, and it is the one animation the
library replaces rather than removes: under prefers-reduced-motion the ring
rests at the size and opacity it spends most of its cycle at, instead of
vanishing.
The node, and the lane it runs in
Section titled “The node, and the lane it runs in”The node is aria-hidden, so nothing that lives only inside it is announced. Put
an icon in it, an image, an Avatar, or the version string of a changelog entry;
leave it empty and it draws the plain dot.
-
09:00
A dot
No node given — the default.
-
09:20
An icon
On the tinted well its role gives it.
-
09:40
Another icon
Same lane, same box, same rail.
One lane for the whole list. A node that grew on its own would bend the rail.
Code
<TimelineItem color="success" node={<CheckIcon />} time="09:20" title="An icon" /><ol class="wayra-timeline" aria-label="Mixed nodes" data-size="md" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="neutral"> <span class="wayra-timeline-time">09:00</span> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">A dot</span> <p class="wayra-timeline-desc">No node given — the default.</p> </div> </li> <li class="wayra-timeline-item" data-color="success"> <span class="wayra-timeline-time">09:20</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3.5 8.5 6.5 11.5 12.5 5"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">An icon</span> <p class="wayra-timeline-desc">On the tinted well its role gives it.</p> </div> </li> <li class="wayra-timeline-item" data-color="info"> <span class="wayra-timeline-time">09:40</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 4.5h8v7h-8zM9.5 7h3l2 2.5v2h-5z"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="11.5" cy="12" r="1.2"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Another icon</span> <p class="wayra-timeline-desc">Same lane, same box, same rail.</p> </div> </li></ol>That is the part worth knowing: the lane is re-pointed by :has() on the root,
never per entry. An icon anywhere in the list widens every node in it; an image or
an Avatar widens it further. A bare dot beside an avatar would otherwise sit off
the line, because the rail crosses the middle of one shared column.
An icon node paints on a well tinted from its role, the way a
List media well does; an image node takes the page colour and
nothing else. Give an Avatar --wayra-avatar-size: 100% and it fills the node.
Layouts
Section titled “Layouts”One axis, so the four are exclusive. The DOM never changes between them — the stamp is a direct child of the entry in every layout, because an entry cannot know what the list asked for. Only the grid columns move.
-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
-
Out for delivery
Three stops ahead of yours.
opposite gives the stamp a column of its own before the rail, end-aligned against it.
Code
<Timeline layout="opposite" aria-label="Delivery status">…</Timeline><ol class="wayra-timeline" aria-label="Opposite layout" data-size="md" data-layout="opposite" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T11:20">11:20</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Packed</span> <p class="wayra-timeline-desc">Boxed at the Ñuñoa depot.</p> </div> </li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <time class="wayra-timeline-time" datetime="2026-08-21T14:05">14:05</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Out for delivery</span> <p class="wayra-timeline-desc">Three stops ahead of yours.</p> </div> </li></ol>-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
- Afternoon
-
Out for delivery
Three stops ahead of yours.
-
Estimated arrival
Not yet left the last stop.
alternate zigzags around a centred rail — and the heading dropped in the middle does not flip the two entries after it.
Code
<Timeline layout="alternate" aria-label="Delivery status"><TimelineItem … /><TimelineItem … /><TimelineGroup>Afternoon</TimelineGroup><TimelineItem … /></Timeline><ol class="wayra-timeline" aria-label="Alternate layout" data-size="md" data-layout="alternate" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T11:20">11:20</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Packed</span> <p class="wayra-timeline-desc">Boxed at the Ñuñoa depot.</p> </div> </li> <li class="wayra-timeline-group">Afternoon</li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <time class="wayra-timeline-time" datetime="2026-08-21T14:05">14:05</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Out for delivery</span> <p class="wayra-timeline-desc">Three stops ahead of yours.</p> </div> </li> <li class="wayra-timeline-item" data-color="primary" data-state="upcoming"> <time class="wayra-timeline-time" datetime="2026-08-21T18:00">18:00</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Estimated arrival</span> <p class="wayra-timeline-desc">Not yet left the last stop.</p> </div> </li></ol>That last clause is the whole reason the parity selector is
:nth-child(odd of .wayra-timeline-item) rather than :nth-child(odd): of S
counts entries and skips the headings and markers sharing the list. The same
mechanism trims the rail stubs — the first entry paints no half above it, the
last none below — so a leading or trailing heading leaves nothing dangling past
the outermost node. of S sits exactly at the browser
floor.
-
Q1
Tokens and themes
Seven palettes, both halves declared.
-
Q2
Form controls
Six controls wired to react-hook-form.
- Q3
-
Q4
Overlays
Modal, Drawer and Tooltip on the top layer.
horizontal lays the rail across the inline axis and turns the list into a scrolling row. snap settles it on the entry nearest the centre.
Code
<Timeline layout="horizontal" snap aria-label="Roadmap">…</Timeline><ol class="wayra-timeline" aria-label="Roadmap" data-size="md" data-layout="horizontal" data-rail="gradient" data-snap="true" style="inline-size: 100%; margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <span class="wayra-timeline-time">Q1</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3.5 8.5 6.5 11.5 12.5 5"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Tokens and themes</span> <p class="wayra-timeline-desc">Seven palettes, both halves declared.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <span class="wayra-timeline-time">Q2</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3.5 8.5 6.5 11.5 12.5 5"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Form controls</span> <p class="wayra-timeline-desc">Six controls wired to react-hook-form.</p> </div> </li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <span class="wayra-timeline-time">Q3</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 4.5h8v7h-8zM9.5 7h3l2 2.5v2h-5z"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="11.5" cy="12" r="1.2"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Data display</span> <p class="wayra-timeline-desc">List, Stat, Table and this timeline.</p> <p class="wayra-timeline-desc"><a href="/components/table/">See the Table page</a></p> </div> </li> <li class="wayra-timeline-item" data-color="primary" data-state="upcoming"> <span class="wayra-timeline-time">Q4</span> <span class="wayra-timeline-node" aria-hidden="true"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 4.5h8v7h-8zM9.5 7h3l2 2.5v2h-5z"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="11.5" cy="12" r="1.2"/></svg></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Overlays</span> <p class="wayra-timeline-desc">Modal, Drawer and Tooltip on the top layer.</p> </div> </li></ol>segments is the default: every entry paints its own two halves, so anything that
paints neither breaks the line. The other three draw one rail on the root
instead, masked to fade out at both ends, and the entries stop painting halves
entirely.
-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
-
Out for delivery
Three stops ahead of yours.
gradient runs the rail from the primary role to the secondary one. line is the same rail in the neutral hairline.
Code
<Timeline rail="gradient" aria-label="Delivery status">…</Timeline><ol class="wayra-timeline" aria-label="Gradient rail" data-size="md" data-rail="gradient" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T11:20">11:20</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Packed</span> <p class="wayra-timeline-desc">Boxed at the Ñuñoa depot.</p> </div> </li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <time class="wayra-timeline-time" datetime="2026-08-21T14:05">14:05</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Out for delivery</span> <p class="wayra-timeline-desc">Three stops ahead of yours.</p> </div> </li></ol>The last three are vertical-only, and the stylesheet says so rather than
pretending: a horizontal list keeps its segments whatever rail asks for, which
is why the roadmap above still has a coloured line between its done entries.
The types say it first. TimelineProps is a union on layout, so a pairing the
stylesheet would ignore does not compile: layout="horizontal" takes
rail="segments" alone and no reveal, and snap is for horizontal only.
Hand-written markup that pairs them anyway still renders, drawn as above.
Groups and the now marker
Section titled “Groups and the now marker”- Today
-
build finished in 42 s
-
deploy published dist/ to the production bucket
- Now
-
scheduled cache purge
log is the compact row: a tiny node, a monospaced stamp beside the message, and a title that truncates.
Code
<Timeline variant="log" aria-label="Deploy log"><TimelineGroup>Today</TimelineGroup><TimelineItem color="success" time="09:41:02" dateTime="…" title={<><strong>build</strong> finished in 42 s</>} /><TimelineNow label="Now" /><TimelineItem state="upcoming" time="09:45:00" title="scheduled cache purge" /></Timeline><ol class="wayra-timeline" aria-label="Deploy log" data-variant="log" data-size="md" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-group">Today</li> <li class="wayra-timeline-item" data-color="success"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41:02">09:41:02</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title"><strong>build</strong> finished in 42 s</span> </div> </li> <li class="wayra-timeline-item" data-color="info"> <time class="wayra-timeline-time" datetime="2026-08-21T09:43:18">09:43:18</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title"><strong>deploy</strong> published dist/ to the production bucket</span> </div> </li> <li class="wayra-timeline-now"> <span class="wayra-timeline-now-dot" aria-hidden="true"></span> <span class="wayra-timeline-now-rule" role="separator" aria-label="Now">Now</span> </li> <li class="wayra-timeline-item" data-color="primary" data-state="upcoming"> <time class="wayra-timeline-time" datetime="2026-08-21T09:45:00">09:45:00</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">scheduled cache purge</span> </div> </li></ol>TimelineGroup is presentational — it names a group visually and adds nothing to
the accessibility tree. A group that must be announced as one is a nested
<ol> with its own aria-label.
TimelineNow is not. It is a real role="separator", and where that role sits is
the interesting part:
<li class="wayra-timeline-now"> <span class="wayra-timeline-now-dot" aria-hidden="true"></span> <span class="wayra-timeline-now-rule" role="separator" aria-label="Now">Now</span></li>The role is on the rule inside the row, never on the <li>. A list may only
own list items, so an <ol> whose direct child carries another role is an axe
only-listitems violation — the marker would break the very list it is marking.
And a separator takes its name from the author rather than from its content, so a
string label becomes aria-label while a ReactNode one renders visibly and
leaves the separator unnamed. Pass a string, or name the rule yourself.
Changelog
Section titled “Changelog”-
Timeline
Four layouts, four rails and the now marker.
-
Table and List
The row grid this timeline reuses.
The node becomes a version chip: fixed width, so the rail still runs through the middle of its column.
Code
<Timeline variant="changelog" aria-label="Release history"><TimelineItem color="success" node="v2.4.0" time="21 Aug 2026" dateTime="2026-08-21" title="Timeline"> <Badge color="success" size="sm">new</Badge></TimelineItem></Timeline><ol class="wayra-timeline" aria-label="Release history" data-variant="changelog" data-size="md" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success"> <time class="wayra-timeline-time" datetime="2026-08-21">21 Aug 2026</time> <span class="wayra-timeline-node" aria-hidden="true">v2.4.0</span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Timeline</span> <p class="wayra-timeline-desc">Four layouts, four rails and the now marker.</p> </div> </li> <li class="wayra-timeline-item" data-color="info"> <time class="wayra-timeline-time" datetime="2026-08-14">14 Aug 2026</time> <span class="wayra-timeline-node" aria-hidden="true">v2.3.1</span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Table and List</span> <p class="wayra-timeline-desc">The row grid this timeline reuses.</p> </div> </li></ol>Tags, cards and images inside an entry are all children — the component ships no
badge slot, no media slot and no action slot. Anything you put there renders under
the description, inside the content column.
The reveal, and what a browser without it gets
Section titled “The reveal, and what a browser without it gets”rail="draw" and reveal="scroll" are scroll-driven CSS: the rail fills as the
list crosses the scrollport and each entry lifts into place, with no observer and
no JavaScript. Both live inside one of the library’s five support queries, and each reads
its whole animation from a variable on the root:
.wayra-timeline { --_wayra-timeline-draw-animation: wayra-timeline-draw linear both; --_wayra-timeline-reveal-animation: wayra-timeline-reveal linear both;}
@supports (animation-timeline: view()) { .wayra-timeline:not([data-layout="horizontal"])[data-rail="draw"]::after { animation: var(--_wayra-timeline-draw-animation); animation-timeline: view(); }
.wayra-timeline[data-reveal="scroll"] .wayra-timeline-item { animation: var(--_wayra-timeline-reveal-animation); animation-timeline: view(); animation-range: entry 0% entry 55%; }
@keyframes wayra-timeline-draw { from { scale: 1 0; } to { scale: 1 1; } }}-
Order confirmed
Card ending 4417 accepted.
-
Packed
Boxed at the Ñuñoa depot.
-
Out for delivery
Three stops ahead of yours.
-
Estimated arrival
Not yet left the last stop.
Scroll inside the box. The scrollport is the consumer's, and so is the link that keeps it keyboard-reachable.
Code
<Timeline rail="draw" reveal="scroll" aria-label="Delivery status">…</Timeline><div style="block-size: 15rem; overflow-y: auto"><ol class="wayra-timeline" aria-label="Scroll-driven timeline" data-size="md" data-rail="draw" data-reveal="scroll" style="inline-size: min(100%, 30rem); margin-inline: auto"> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T11:20">11:20</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Packed</span> <p class="wayra-timeline-desc">Boxed at the Ñuñoa depot.</p> </div> </li> <li class="wayra-timeline-item" data-color="info" data-state="current"> <time class="wayra-timeline-time" datetime="2026-08-21T14:05">14:05</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Out for delivery</span> <p class="wayra-timeline-desc">Three stops ahead of yours.</p> </div> </li> <li class="wayra-timeline-item" data-color="primary" data-state="upcoming"> <time class="wayra-timeline-time" datetime="2026-08-21T18:00">18:00</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Estimated arrival</span> <p class="wayra-timeline-desc">Not yet left the last stop.</p> </div> </li></ol><p style="text-align: center"><a href="/components/list/">The whole history</a></p></div>The fallback is complete because everything the block adds is additive. Below
the tier, draw is the static masked hairline the rule above the block already
draws, and reveal does nothing at all — every entry is simply visible, in place,
at full opacity. Nothing is left hidden waiting for an animation that will never
run, which is the failure mode this shape exists to avoid.
Reduced motion is one block, outside the support query, so it covers both tiers.
Under prefers-reduced-motion: reduce it sets both variables to none. The rules
themselves are never restated, so the rescue cannot lose to them on specificity.
The empty rail is the first keyframe rather than a declaration on the rule, so
with the animation gone the rail is drawn full-height: a reader who asked for less
motion still gets a finished rail rather than an empty one. The same block sets
the hover transition to 0s and brings the current node’s pulse to rest as the
ring it spends most of its cycle as.
reveal is vertical-only for a mechanical reason, not a stylistic one: view()
resolves on the block axis, and a horizontal scrollport has no block scroll range
for it to read.
Styling contract
Section titled “Styling contract”The class plus the data-* attributes are public API — every preview on this page
is hand-authored HTML with no React in the page:
<ol class="wayra-timeline" aria-label="Delivery status" data-size="md"> <li class="wayra-timeline-group">Today</li> <li class="wayra-timeline-item" data-color="success" data-state="done"> <time class="wayra-timeline-time" datetime="2026-08-21T09:41">09:41</time> <span class="wayra-timeline-node" aria-hidden="true"></span> <div class="wayra-timeline-content"> <span class="wayra-timeline-title">Order confirmed</span> <p class="wayra-timeline-desc">Card ending 4417 accepted.</p> </div> </li></ol>variant, layout and rail always write their resolved value, default,
start and segments included, the way every defaulted attribute in the library
does. The stylesheet reads a missing attribute as the default, so hand-written
markup may leave all three off, as the one above does.
| Property | Default | What it does |
|---|---|---|
--wayra-timeline-node |
calc(var(--wayra-size-selector) * 3.2) at md |
The node’s diameter, and where the rail crosses. |
--wayra-timeline-gap |
calc(var(--wayra-size-field) * 3.5) |
Column gap, and the space under an entry. |
--wayra-timeline-pad |
calc(var(--wayra-size-field) * 1.5) |
Block padding on an entry. |
--wayra-timeline-line-w |
2px |
The rail’s thickness, and the ring on a hollow node. |
--wayra-timeline-time-col |
4.5rem |
The stamp’s own column under opposite. |
API reference
Section titled “API reference”Timeline
Section titled “Timeline”TimelineProps is a union on layout (see Rails) whose two halves
both extend ComponentProps<"ol">, so id, style, ref, className and
aria-label land on the <ol>.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "changelog" | "log" |
"default" |
changelog turns the node into a version chip; log is a compact monospaced row. |
layout |
"start" | "opposite" | "alternate" | "horizontal" |
"start" |
Where the content sits against the rail. |
size |
"sm" | "md" | "lg" |
"md" |
The node’s diameter and the entry’s type step. |
rail |
"segments" | "line" | "gradient" | "draw" |
"segments" |
Per-entry halves, or one rail on the root. The last three are vertical-only. |
reveal |
"scroll" |
— | Fades and lifts each entry as it enters the scrollport. Vertical only. |
snap |
boolean |
false |
Snaps the scrolling row to the centred entry. horizontal only. |
children |
ReactNode |
— | TimelineItem, TimelineGroup, TimelineNow. Required. |
unstyled |
boolean |
false |
Drops the wayra-timeline class, keeps every attribute. |
TimelineItem
Section titled “TimelineItem”TimelineItemProps extends ComponentProps<"li"> minus color and title —
both names are taken by props below.
| Prop | Type | Default | Description |
|---|---|---|---|
color |
WayraColor |
"primary" |
Node, done rail and current title. Emitted as data-color. |
state |
"done" | "current" | "upcoming" |
— | A look. Emitted as data-state when given. |
interactive |
boolean |
false |
Hover wash on the content block. Wires no handlers and no keyboard. |
headingLevel |
2 | 3 | 4 | 5 | 6 |
— | Renders the title as a real heading instead of a <span>. |
node |
ReactNode |
— | What goes inside the node. Empty draws the plain dot. aria-hidden. |
time |
ReactNode |
— | The visible stamp. |
dateTime |
string |
— | Machine stamp; with it the slot is a real <time datetime>. |
title |
ReactNode |
— | The entry’s main line. |
description |
ReactNode |
— | A paragraph under the title, muted. |
year |
ReactNode |
— | A large year behind the content as a watermark. Decoration: aria-hidden and inert. |
children |
ReactNode |
— | Anything else, under the description, inside the content column. |
unstyled |
boolean |
false |
Drops wayra-timeline-item and every part class on this entry. |
TimelineGroup and TimelineNow
Section titled “TimelineGroup and TimelineNow”Both extend ComponentProps<"li"> and both render an <li>.
| Prop | Type | Description |
|---|---|---|
children |
ReactNode |
TimelineGroup only: the heading text. Required. |
label |
ReactNode |
TimelineNow only: the marker’s visible text, and the separator’s accessible name when it is a string. Required. |
unstyled |
boolean |
Drops the class on that instance. |
unstyled is per instance: an entry inside an unstyled list keeps its own class
unless it opts out too.
Accessibility
Section titled “Accessibility”- It is a real ordered list. An
<ol>whose children are all<li>— entries, headings and the now marker alike. Name it witharia-label, or pointaria-labelledbyat a heading above it. - The separator is on an inner span. Putting
role="separator"on the<li>would make the list own a non-list-item, which is the axe ruleonly-listitems. The row stays a list item; the rule inside it carries the role and the name. - The node says nothing. It is
aria-hidden, along with theyearwatermark. Anything that only lives in a node or a watermark is not announced. stateis not an announcement. “Done”, “current” and “upcoming” are colours and strokes. Put the word in the entry’s own text if it matters.- Headings are opt-in. A title is a
<span>unless you passheadingLevel— an entry is a row far more often than it is a document section, and inventing an<h3>per entry would wreck the page’s heading outline. - Contrast. In the default theme, titles are
base-contentonbase-100(16.9:1 light, 15.1:1 dark); descriptions, stamps, group headings and anupcomingentry’s text take the shared muted colour (5.64:1 / 6.15:1). Acurrenttitle is the role solid and the now marker is always the primary solid — both on the page, where the colour contract holds 4.5:1. Theyearwatermark is 7% ink, measured at 1.15:1 / 1.20:1 (1.13–1.20:1 across the seven themes): WCAG 1.4.3 exempts it as decoration. axe checksaria-hiddentext for contrast, so the watermark is alsoinert, which axe skips. Write the markup by hand the same way:<span class="wayra-timeline-year" aria-hidden="true" inert>. - Forced colors. Rails and node borders take
CanvasText(thegradientrail falls back to a solid one), anupcomingnode goes back toCanvaswith aCanvasTextring, and the now marker’s dot, text and rule all takeCanvasText. - Right to left. Every measurement is logical, so the rail and the zigzag
mirror on their own. The now marker’s fade is a physical gradient, so it turns
under a
dir="rtl"on the marker or any ancestor, which the browser floor understands;:dir()(Chrome 120) only puts an LTR island back. - Reduced motion is one block that covers both tiers: the two scroll-driven
animations are dropped and the drawn rail rests full-height, the hover
transition goes to
0s, and thecurrentpulse rests as a ring.
Do and don’t
Section titled “Do and don’t”Do
-
09:41
Packed
- Now
-
18:00
Arrival
role="separator" on the rule inside the . The list still owns nothing but list items.
Don't
-
09:41
Packed
- Now
-
18:00
Arrival
The role hoisted onto the . The now owns a child that is not a list item — an axe only-listitems violation, and the item count is wrong.
Do
-
11:20
Packed — done
The word is in the text, not only in the colour.
-
18:00
Arrival — pending
Announced as written.
The state is in the entry's own words. A screen reader reads “Packed — done”.
Don't
-
11:20
Packed
-
18:00
Arrival
Two entries that read identically to a screen reader. The filled rail and the hollow node are paint; data-state is in no accessibility tree.