Build / Molecules
EventTimeline
The events a set of cards came from, laid out as one line through time.
Picking an event on the line changes what is shown below it — on the Connections page, which pile of cards is on the desk. A line rather than a row of tabs because the ordering is the point: these events happened in a sequence, some of them are still ahead, and the gap between "last autumn" and "next January" is a fact worth drawing.
It renders as a radio group, so arrow keys walk the line and the selection is announced with its date, its place, and whether it has happened yet. Position on the line is never the only carrier of that — each event spells out "past" or "still to come" for a screen reader, and the selected event is bolder and larger, not merely greener.
On a phone the line has nowhere to go: three labels side by side on a 375px screen are unreadable. So it collapses to the dots alone, tapped like pagination, with the selected event named in full underneath. The labels stay in the accessibility tree either way, since they are what give each dot its name.
orientation stands the line on end. A rail is the same events in the
same order, in the shape a narrow column beside the content can hold — one
row per event, with room for the date and the place on lines of their own.
It is a decision about the space, so the caller makes it; responsive
takes the line to a rail at lg, where a page has the width for one.
import { EventTimeline } from '@meetcard/deck'Example
<EventTimeline orientation="responsive" value={eventId} onValueChange={setEventId} events={[ { id: 'saastr', name: 'SaaStr Annual', date: '2027-09-09', location: 'San Francisco, CA' }, { id: 'founders', name: 'Founders Dinner', date: '2027-11-12', location: 'Denver, CO' }, ]} />Stories
The line as it reads mid-season: two events behind, one still ahead, and the most recent one selected.
Source
const Default = () => <div style={{ maxWidth: 720 }}>
<EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()} />
</div>;Every event still ahead — a calendar filled in before anyone was met.
Source
const AllUpcoming = () => <div style={{ maxWidth: 720 }}>
<EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-01-01"
defaultValue="saastr-annual"
locale="en-US"
onValueChange={fn()} />
</div>;A line long enough that the labels have to share the width.
Source
const ManyEvents = () => <div style={{ maxWidth: 720 }}>
<EventTimeline
events={[
{ id: 'devday', name: 'DevDay', date: '2027-06-02', location: 'Portland, OR' },
...events.slice(0, 2),
{ id: 'seed-night', name: 'Seed Night', date: '2027-11-30', location: 'Boulder, CO' },
...events.slice(2),
{ id: 'ops-forum', name: 'Ops Forum', date: '2028-03-04', location: 'Toronto, ON' },
]}
today="2027-12-01"
defaultValue="seed-night"
locale="en-US"
onValueChange={fn()} />
</div>;Picking an event reports the new selection.
Source
const SelectingAnEvent = () => <div style={{ maxWidth: 720 }}>
<EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()} />
</div>;Arrow keys walk the line — it is a radio group, so this comes for free.
Source
const KeyboardWalksTheLine = () => <div style={{ maxWidth: 720 }}>
<EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()} />
</div>;Phone. Three labels side by side on a 375px screen are unreadable, so the line collapses to its dots — tapped like pagination — with the selected event named in full underneath. The labels stay in the accessibility tree, since they are what give each dot its name.
Source
const Mobile = () => <EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()} />;The line stood on end, for a rail beside the thing it indexes. Each event gets a row with room for its date and place on lines of their own, and the selected one is a surface rather than a slightly bigger dot — in a column of near-identical marks, the block of colour is what you find from across the page.
Source
const Vertical = () => <div style={{ maxWidth: 272 }}>
<EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()}
orientation="vertical"
label="Where we met" />
</div>;responsive — the line until lg, the rail above it. The page decides it
has the width for a rail; the timeline decides what a rail looks like.
Pinned to a narrow viewport here, so what renders is the line: the rail is the same component past 1024px.
Source
const Responsive = () => <EventTimeline
events={[
{
id: 'saastr-annual',
name: 'SaaStr Annual',
date: '2027-09-09',
location: 'San Francisco, CA',
},
{
id: 'founders-dinner',
name: 'Founders Dinner',
date: '2027-11-12',
location: 'Denver, CO',
},
{
id: 'revops-summit',
name: 'RevOps Summit',
date: '2028-01-22',
location: 'Chicago, IL',
},
]}
today="2027-12-01"
defaultValue="founders-dinner"
locale="en-US"
onValueChange={fn()}
orientation="responsive"
label="Where we met" />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
eventsrequired | TimelineEvent[] | — | The events on the line, oldest first. Left in the caller's order rather than sorted here: a timeline that silently reorders its own input hides a bug in whatever produced it. |
defaultValue | string | — | Uncontrolled starting selection. Defaults to the first event. |
label | string | 'Event timeline' | Accessible (and, from sm up, visible) name for the line. |
locale | string | — | — |
onValueChange | (id: string) => void | — | — |
orientation | 'horizontal' | 'vertical' | 'responsive' | 'horizontal' | Which way the line runs. Default horizontal. Pass responsive on a
page that puts the line in a rail once it has the width for one. |
today | string | todayISO() | ISO date the line is read from — everything after it is still to come. Defaults to today, and exists so a story or a test can stand somewhere fixed on the line. |
value | string | — | Controlled selection, by event id. |
Also accepts everything from Omit, so id, data-* and event handlers pass straight through.