Event timeline
What happened, when and by whom, with each correction shown against the entry it corrects.
On this page
Saturday 17 October 2026
- LogLizard light abeam to port, 4.2 NM49°55.8′ N 005°12.1′ W, COG 088°, SOG 6.4 kn
- VoiceWind 18 kn from 230°, one reef in the mainCorrected 02:25 UTC by BertWind: 15 kn → 18 knReason: Read the gust, not the mean
- Watch handed over from Ada to BertCourse 245°, barometer 1012 hPa and steady, traffic clear.
Friday 16 October 2026
- Left the berth at Brixham
Code
import { EventTimeline } from "@bosunkit/components/event-timeline";
import type { ReactElement } from "react";
import { LOG } from "./log-data";
export const description: string =
"A night passage's log over two days, newest first, with a wind entry corrected after the gust was read.";
export default function Showcase(): ReactElement {
return <EventTimeline events={LOG} timeZone="UTC" timeReference="UTC" label="Ship's log" />;
}
An event timeline lists what happened, when and by whom: the ship's log, an alert history, a watch's handovers. It
sorts its entries by time, newest first unless order says otherwise, and puts each day under a heading in the crew's
language. Every time is printed with its timeReference. An entry carries what happened, more about it, who recorded
it and where it came from; end takes a badge or a menu.
A correction is shown against the entry it corrects, with when and by whom, the field, the value first recorded struck
through and the value it holds, and why: a record that was corrected never reads as if it had always said the new
value, and a screen reader hears the change as one sentence. An alert in a history carries its priority's shape, color
and word (IEC 62923), and when it was acknowledged and cleared; it never flashes. With onSelect each entry is a button,
the selected one marked as current. hasEarlier and loadEarlierAction page back, the button pending until the
action settles. A new entry fades in as it arrives, and nothing already listed moves.
relative writes how long ago each entry was beside its time ("12:00 UTC · 1 hr ago") within relativeWithinMs, 12
hours by default, floored; the time keeps its reference, and its tooltip writes the moment in full. An entry's actions
show as icon buttons named by their label, two of them, with the rest in a "More actions" menu, and report through
onAction; they show while the pointer is over the entry or focus is in it, always on a touch screen and on the
selected entry, and they never select it. Its source and tags are chips, and filterable draws a row of chips for the
priorities, sources and tags the entries carry: the pressed chips show the entries that match any of them.
Examples
Relative times, actions and filters
Saturday 17 October 2026
- LogLizard light abeam to port, 4.2 NM49°55.8′ N 005°12.1′ W, COG 088°, SOG 6.4 kn
- VoiceWind 18 kn from 230°, one reef in the mainCorrected 02:25 UTC by BertWind: 15 kn → 18 knReason: Read the gust, not the mean
- Watch handed over from Ada to BertCourse 245°, barometer 1012 hPa and steady, traffic clear.
Friday 16 October 2026
- Left the berth at Brixham
The page receives: nothing yet
Code
import { EventTimeline, type EventTimelineAction, type EventTimelineEvent } from "@bosunkit/components/event-timeline";
import { Copy, Pencil, Share2 } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
import { LOG } from "./log-data";
export const description: string =
"Relative times beside each time, actions on hover or focus (correct, copy, and share in the menu), and chips that " +
"filter the log by source and tag.";
const NOW: number = Date.UTC(2026, 9, 17, 8, 30);
const ACTIONS: EventTimelineAction[] = [
{ id: "correct", label: "Correct", icon: <Pencil /> },
{ id: "copy", label: "Copy", icon: <Copy /> },
{ id: "share", label: "Share", icon: <Share2 /> },
];
const EVENTS: EventTimelineEvent[] = LOG.map(
(event: EventTimelineEvent): EventTimelineEvent => ({ ...event, actions: ACTIONS }),
);
export default function ActionsAndFilters(): ReactElement {
const [last, setLast]: [string, Dispatch<SetStateAction<string>>] = useState<string>("nothing yet");
return (
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<EventTimeline
events={EVENTS}
timeZone="UTC"
timeReference="UTC"
relative
now={NOW}
filterable
onAction={(eventId: string, actionId: string): void => {
setLast(`${actionId} on ${eventId}`);
}}
/>
<p style={{ margin: 0, color: "var(--text-dim)" }}>The page receives: {last}</p>
</div>
);
}
An alert history
- AlarmAnchor watchAnchor drag: 38 m from the drop pointAcknowledged 22:32 UTC by AdaCleared 22:50 UTC
- WarningWeatherWind rising: 24 kn, gusts 31 knAcknowledged 21:52 UTC by Ada
- CautionBattery monitorHouse battery at 48 %Cleared 20:30 UTC
Code
import { EventTimeline, type EventTimelineEvent } from "@bosunkit/components/event-timeline";
import type { ReactElement } from "react";
export const description: string =
"The alerts of a night at anchor, each with its priority's shape, color and word, acknowledged and cleared.";
const NIGHT: number = Date.UTC(2026, 9, 16, 22, 30);
const MINUTE: number = 60_000;
const ALERTS: EventTimelineEvent[] = [
{
id: "drag",
at: NIGHT,
title: "Anchor drag: 38 m from the drop point",
priority: "alarm",
source: "Anchor watch",
acknowledged: { at: NIGHT + 2 * MINUTE, by: "Ada" },
cleared: { at: NIGHT + 20 * MINUTE },
},
{
id: "wind",
at: NIGHT - 40 * MINUTE,
title: "Wind rising: 24 kn, gusts 31 kn",
priority: "warning",
source: "Weather",
acknowledged: { at: NIGHT - 38 * MINUTE, by: "Ada" },
},
{
id: "battery",
at: NIGHT - 3 * 60 * MINUTE,
title: "House battery at 48 %",
priority: "caution",
source: "Battery monitor",
cleared: { at: NIGHT - 2 * 60 * MINUTE },
},
];
export default function AlertHistory(): ReactElement {
return <EventTimeline events={ALERTS} timeZone="UTC" timeReference="UTC" label="Alert history" groupBy="none" />;
}
Selectable entries
Friday 16 October 2026
Saturday 17 October 2026
- Course 245°, barometer 1012 hPa and steady, traffic clear.
- VoiceCorrected 02:25 UTC by BertWind: 15 kn → 18 knReason: Read the gust, not the mean
- Log49°55.8′ N 005°12.1′ W, COG 088°, SOG 6.4 kn
Code
import { EventTimeline } from "@bosunkit/components/event-timeline";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
import { LOG } from "./log-data";
export const description: string = "Entries as buttons in a side panel, oldest first; the selected one is marked current.";
export default function Selectable(): ReactElement {
const [selected, setSelected]: [string, Dispatch<SetStateAction<string>>] = useState<string>("wind");
return (
<div style={{ maxInlineSize: "28rem" }}>
<EventTimeline
events={LOG}
timeZone="UTC"
timeReference="UTC"
order="oldest"
dense
selectedId={selected}
onSelect={setSelected}
/>
</div>
);
}
Earlier entries
Saturday 17 October 2026
- LogLizard light abeam to port, 4.2 NM49°55.8′ N 005°12.1′ W, COG 088°, SOG 6.4 kn
- VoiceWind 18 kn from 230°, one reef in the mainCorrected 02:25 UTC by BertWind: 15 kn → 18 knReason: Read the gust, not the mean
Code
import { EventTimeline, type EventTimelineEvent } from "@bosunkit/components/event-timeline";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
import { LOG } from "./log-data";
export const description: string = "The two latest entries, and a button that loads the earlier ones from bosund.";
export default function Earlier(): ReactElement {
const [events, setEvents]: [EventTimelineEvent[], Dispatch<SetStateAction<EventTimelineEvent[]>>] = useState<
EventTimelineEvent[]
>(
LOG.slice(0, 2),
);
return (
<EventTimeline
events={events}
timeZone="UTC"
timeReference="UTC"
hasEarlier={events.length < LOG.length}
loadEarlierAction={(): Promise<void> =>
new Promise<void>((resolve: () => void): void => {
setTimeout((): void => {
setEvents(LOG);
resolve();
}, 600);
})
}
/>
);
}
Nothing recorded
Nothing recorded yet
Code
import { EventTimeline } from "@bosunkit/components/event-timeline";
import type { ReactElement } from "react";
export const description: string = "A log with no entries: the timeline's empty state.";
export default function Empty(): ReactElement {
return <EventTimeline events={[]} timeZone="UTC" timeReference="UTC" label="Ship's log" />;
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| eventsrequired | readonly EventTimelineEvent[] | – | What happened, in any order: the timeline sorts them by at. |
| timeReferencerequired | string | – | The reference every time is printed with, translated by the caller ("UTC", "ship's time"). |
| timeZone | string | undefined | – | The time zone times and days are written in, an IANA name; the browser's unless set. Match it to the reference. |
| order | "newest" | "oldest" | undefined | "newest" | newest first, as a log is read, or oldest first, as a passage is told; newest unless set. |
| groupBy | "day" | "none" | undefined | "day" | day puts each day under its own heading; none lists them together. day unless set. |
| headingLevel | NestedHeadingLevel | undefined | 3 | The heading level of the day headings; 3 unless set. |
| onSelect | ((id: string) => void) | undefined | – | Makes each entry a button: called with its id when the crew press it. |
| selectedId | string | undefined | – | The entry shown as the current one, with onSelect. |
| onAction | ((eventId: string, actionId: string) => void) | undefined | – | Called with an entry's id and an action's id when the crew press one of the entry's actions. |
| relative | boolean | undefined | false | Writes how long ago each entry was beside its time ("12:00 UTC · 1 hr ago"), floored, for entries within
relativeWithinMs; the time and its reference stay printed. The timeline keeps a minute clock unless now is set. |
| relativeWithinMs | number | undefined | 12 * HOUR | How far back, in milliseconds, an entry gets its relative time; 12 hours unless set. |
| now | number | undefined | – | A fixed current time for the relative times, for examples and printouts. |
| filterable | boolean | undefined | false | Draws a row of chips over the entries, one for each priority, source and tag they carry: pressing chips shows the entries that match any pressed chip, and "Clear filters" shows them all again. |
| hasEarlier | boolean | undefined | false | Whether there are earlier entries than these; shows a button that calls loadEarlierAction. |
| loadEarlierAction | (() => Promise<void>) | undefined | – | Loads earlier entries; the button shows it is pending until the promise settles. |
| empty | ReactNode | – | What it shows when there are no entries; an empty state saying nothing is recorded unless set. |
| dense | boolean | undefined | false | Tighter rows, for a side panel. |
| label | string | undefined | – | The timeline's name, translated by the caller ("Ship's log"); "Timeline" unless set. |
| motion | MotionProp | undefined | true | How a new entry arrives and a removed one leaves: by default it fades; a preset or { enter, exit } chooses;
false turns it off. An entry already there never moves. |
| className | string | undefined | – | Classes added to the timeline's own. |
| testId | string | undefined | – | Becomes data-testid on the root. |
Related
- State timeline: states along a time axis, such as an engine's running hours.
- Alert list: the alerts that are active.
- Empty state: what the timeline shows with no entries.