Timeline
Vertical chronological event feed for activity logs, audit trails, release notes, and notification streams. Pure semantic <ol>/<li> with a status-colored indicator and an optional icon override.
Activity log
- Pull request merged2 hours agofeat(docs): Phase 2b polish — form input demos
- Build failed on Vercel3 hours agoAttempted to call cn() from the server but cn is on the client
- Review requested4 hours ago@oscarabcorona requested review from @reviewer
- Branch createdyesterdayft/component-polish-form-inputs branched from main
- Approaching API rate limit5 hours agoGitHub REST quota at 85% — switch to GraphQL or wait 12 min for reset.
Release notes feed
- v1.4.0 — Per-component bundle splitApr 28RSC-safe deep imports, 14 KB shaved from the cold-start bundle.
- v1.3.0 — Tokens and themes alignmentApr 14Synced with @hex-core/tokens 1.2.0; new midnight + ember presets.
Installation
pnpm dlx @hex-core/cli add timelineCustom icon
Override the default dot with a custom node
import { Timeline } from "@/components/ui/timeline";
export function Example() {
return (
<Timeline
aria-label="Release notes"
events={[
{ id: "v1", title: "v1.0", timestamp: "Apr 24", icon: <span>⚡</span> },
{ id: "v2", title: "v1.1", timestamp: "Apr 27", icon: <span>🐛</span>, status: "warning" },
]}
/>
);
}Grouped by day
Group events by date in the parent and render a sub-Timeline per day. Each cluster gets its own h3 heading so the timeline scans like a journal rather than one long flat feed.
import { useMemo } from "react";
import { Timeline, type TimelineEvent } from "@/components/ui/timeline";
interface DatedEvent extends TimelineEvent {
occurredAt: string; // ISO date
}
const events: DatedEvent[] = [
{ id: "a", occurredAt: "2026-04-26T09:00:00Z", title: "Branch created", timestamp: "9:00 AM", status: "info" },
{ id: "b", occurredAt: "2026-04-26T14:20:00Z", title: "PR opened", timestamp: "2:20 PM", status: "info" },
{ id: "c", occurredAt: "2026-04-27T10:05:00Z", title: "Review approved", timestamp: "10:05 AM", status: "success" },
{ id: "d", occurredAt: "2026-04-27T11:42:00Z", title: "Merged to main", timestamp: "11:42 AM", status: "success" },
];
const dayLabel = new Intl.DateTimeFormat("en-US", { dateStyle: "full" });
function groupByDay(items: DatedEvent[]) {
const buckets = new Map<string, DatedEvent[]>();
for (const item of items) {
const key = item.occurredAt.slice(0, 10);
const bucket = buckets.get(key) ?? [];
bucket.push(item);
buckets.set(key, bucket);
}
return [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));
}
export function GroupedActivity() {
const groups = useMemo(() => groupByDay(events), []);
return (
<div className="space-y-6">
{groups.map(([day, dayEvents]) => (
<section key={day} className="space-y-3">
<h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{dayLabel.format(new Date(day))}
</h3>
<Timeline aria-label={`Activity on ${day}`} events={dayEvents} />
</section>
))}
</div>
);
}Variant values
sizeIndicator sizesm- First eventnow
- Second event5 min ago
md- First eventnow
- Second event5 min ago
| Value | Description |
|---|---|
sm | Compact 1.25rem indicator |
mddefault | Default 1.75rem indicator |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
eventsrequired | object | — | Ordered list of { id, title, timestamp?, description?, icon?, status? } events. |
size | string | md | Indicator size: 'sm' | 'md' |
aria-labelrequired | string | — | Required accessible name for the ordered list (e.g. 'Activity log', 'Release notes') |
AI Guidance
When to use
Use to show a chronological event feed: activity logs, audit trails, release notes, notification history, ticket events. Each event has a title and optional timestamp + description.
When not to use
Don't use for project schedules / Gantt charts (build a custom layout). Don't use for navigation between time periods (use Tabs or Stepper). Don't use for paginated data (use Table or DataTable). Don't use for >50 events without virtualization — Timeline renders every item.
Common mistakes
- Forgetting aria-label — the <ol> needs an accessible name to be understood as a feed
- Using duplicate event ids — breaks React keys and event reconciliation on re-render
- Stuffing the description with rich layouts that overflow the timeline rail — keep it short or move to a Card
- Setting status='error' on every event for emphasis — color loses meaning when overused
- Mixing controlled timestamps as Date objects without formatting — Timeline accepts ReactNode, so format upstream (date-fns) before passing in
Accessibility
Renders <ol> with the provided aria-label. The status-colored indicator and connector line are aria-hidden — meaning is carried entirely by the title/timestamp/description text. No aria-current; events are historical, not navigational. For >50 events consider a windowing solution outside Timeline.
Token budget: 1100
Verified against @hex-core/components@1.12.0