# Timeline

> Follow what happened, newest first, grouped by day.

- Category: Tables and timeline
- Import: `import { Timeline } from "@sagui/ui";`
- Page: /components/timeline

```tsx
import { Timeline } from "@sagui/ui";
import { activity, timelineNow } from "./sample-data";

export function Hero() {
  return (
    <div className="w-full max-w-lg">
      <Timeline events={withIcons} now={timelineNow} label="Project activity" />
    </div>
  );
}
```

## When to use

- Project history, audit logs, and deploy streams where recency matters.
- Live feeds where new updates slide in at the top.
- Rows that expand in place to show logs or detail.

## When not to use

- Use sortable-data-table when people sort or compare.
- Use stepper for progress through fixed steps.

## Installation

Install the package and import the styles once. The [installation guide](/docs/installation) covers the Tailwind setup.

```bash
npm install @sagui/ui
```

## Usage

```tsx
import { Timeline } from "@sagui/ui";

const events = [
  { id: "e1", at: "2026-09-22T09:12:00Z", actor: "Maya", title: "merged Checkout redesign into main", meta: "PR #482" },
  { id: "e2", at: "2026-09-22T08:40:00Z", title: "Deploy failed", tone: "danger" as const, detail: <pre>Build step exited 1</pre> },
];

export function Activity({ now }: { now: number }) {
  return <Timeline events={events} now={now} label="Project activity" maxHeight={420} />;
}
```

## Examples

### New updates arrive

Newest shows first. When an update arrives the feed scrolls back to the top, unless `scrollToNew` is false.

```tsx
import { useState } from "react";
import { GitCommitHorizontal } from "lucide-react";
import { Button, Timeline, TimelineEvent } from "@sagui/ui";
import { activity, timelineNow } from "./sample-data";

export function Live() {
  const [events, setEvents] = useState<TimelineEvent[]>(withIcons.slice(2));
  const [now, setNow] = useState(timelineNow);
  function add() {
    const at = now + 60_000;
    setNow(at);
    setEvents((list) => [{ id: `n${at}`, at, actor: "You", title: "pushed 2 commits to main", meta: "Just now", icon: <GitCommitHorizontal size={14} />, tone: "success" }, ...list]);
  }
  return (
    <div className="grid w-full max-w-lg gap-4">
      <Button variant="outline" size="sm" className="w-fit" onClick={add}>Add an update</Button>
      <Timeline events={events} now={now} label="Project activity" maxHeight={360} />
    </div>
  );
}
```

## API reference

### Timeline

A vertical activity feed grouped by day with pinned day labels and rows that expand in place.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `events` (required) | `{ id: string; at: string \| number; actor?: string; title: string; meta?: string; detail?: ReactNode; avatar?: string; icon?: ReactNode; tone?: "neutral" \| "success" \| "danger" }[]` | – | Updates in any order; newest shows first. Rows with detail are expandable. |
| `now` (required) | `number` | – | Reference time in epoch ms for relative labels and day groups. Pass a ticking clock to keep labels fresh. |
| `label` (required) | `string` | – | Accessible name for the feed. |
| `timeZone` | `string` | `"UTC"` | Time zone for day groups and clock times. |
| `locale` | `string` | `"en-US"` | Formatting locale. |
| `maxHeight` | `number \| string` | – | Height of the scrolling area. Without it the feed grows with the page. |
| `scrollToNew` | `boolean` | `true` | Scrolls back to the top when a new update arrives. |
| `defaultExpanded` | `string[]` | `[]` | Event ids expanded on mount. |
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `3` | Heading level for day labels. |
| `className` | `string` | – | Class for the root. |

## Keyboard interactions

| Keys | Action |
| --- | --- |
| ArrowDown / ArrowUp | Moves between expandable rows. |
| Home / End | Jumps to the first or last expandable row. |
| Enter / Space | Expands or collapses the focused row. |

## Accessibility

- The feed is a labelled role="region"; day labels are headings at headingLevel with a hidden update count.
- Expandable rows are buttons with aria-expanded and aria-controls; rows without detail are not interactive.
- Times use a time element with the full date in hidden text, and new updates are announced in a status region.
- Say the outcome in the title, since tone is color only.

## Motion

- The connecting line draws as rows come into view and markers pop in.
- New updates slide in at the top while the rest glide down; details expand on a spring.
- Reduced motion replaces travel with short fades and instant height changes.

## Responsive behavior

- Day labels stay pinned while updates scroll under them, inside maxHeight or the page.
- Below 420px row padding tightens so text keeps its width.

## Performance

- Events are not virtualized; set maxHeight for long feeds and trim old events.
- Pass a now that ticks about once a minute rather than every second, since it re-renders every row.

## Notes

- Use for project history, audit logs, and deploy streams where order and recency matter. Use sortable-data-table when people sort or compare.
- Pass a ticking now (for example updated every minute) so relative times stay current, and a fixed timeZone to avoid hydration mismatches.

## Related

- [Sortable data table](/components/sortable-data-table): Compare structured records with sortable columns.
- [Avatar](/components/avatar): A compact identity marker for people and accounts.
- [Badge](/components/badge): A small label for status, category, or metadata.
