Source: https://monarchui.dev/components/activity-timeline/

Components · Collaboration

# ActivityTimeline

Vertical feed of events: actor, verb, object, time and marker, grouped by day, with expandable details and filters.

@rookcom/monarch-react/collaboration

Component

## Task activity with filters and details

## Usage examples

### Compact, with avatars

## Purpose

Tells the story of a record or a workspace as a readable list of sentences (“Ana Lopez moved Pricing page to In review”), with who did it, when, and optional details. One shared pattern, so every product’s history reads the same way.

### Use when

- A record’s history: a ticket, task, client, incident or IP record.
- Project or workspace activity, with comments and changes interleaved.
- Status-page incident updates (a stage per event, automated updates tagged).

### Avoid when

- A live conversation with replies and reactions: use MessageTimeline and MessageRow.
- A security audit trail that needs IP, device and export: use AuditLogViewer.
- Unread inbox items: use NotificationCenter.

## Design principles

- Events read as sentences: actor in strong text, then the verb, then the object.
- People and automation are always distinguishable: automation and system actors carry an “Automated” tag in text.
- Detail on demand: diffs and payloads sit behind a disclosure, so the feed stays scannable.
- The product humanises events; Monarch lays them out. Verbs and objects are nodes the product writes.

## API reference

```tsx
import { ActivityTimeline } from '@rookcom/monarch-react/collaboration';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `ActivityTimeline` | component | client | `.mn-activity-timeline` |

Each event: `id`, `verb`, `time`, and optional `actor`, `object`, `icon`, `tone`, `meta`, `body`, `details`.

`expandedIds` / `defaultExpandedIds` / `onExpandedChange` control the open details.

The ref is forwarded to the root `<div>`.

Generated from the TypeScript declarations of `packages/react/src/components/collaboration/activity-timeline`.

## Properties

### `ActivityTimeline`

A vertical feed of events: actor, verb, object, time and a marker, grouped by day, with expandable details, a filter slot and load more. For record histories, audit trails in context and project activity.

| Prop | Type | Description |
| --- | --- | --- |
| `events` required | `readonly ActivityEvent[]` | Events, newest first (or oldest first: order is kept). |
| `className` | `string` |  |
| `dayHeadingLevel` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | Heading level of the day labels. Default `h3`. |
| `defaultExpandedIds` | `readonly string[]` |  |
| `density` | `"default" \| "compact"` | `compact` tightens spacing for side panels. |
| `empty` | `ReactNode` | Replaces the default empty text. |
| `error` | `ReactNode` | Failure, e.g. an InlineErrorRetry. Replaces the feed. |
| `expandedIds` | `readonly string[]` | Open details, controlled. |
| `filters` | `ReactNode` | Filter controls above the feed (TabsFilter, FilterBar, a Select). The product filters `events`. |
| `groupByDay` | `boolean` | Group under Today, Yesterday and dates. Default `true`. |
| `hasMore` | `boolean` | Another page exists. Shows LoadMore after the feed. |
| `id` | `string` |  |
| `loading` | `boolean` | First load: shows a skeleton. |
| `loadingMore` | `boolean` |  |
| `loadMoreError` | `boolean` |  |
| `locale` | `string` |  |
| `markers` | `"dot" \| "avatar"` | `dot` (default) or the actor's `avatar` as the marker. |
| `messages` | `Partial<ActivityTimelineMessages>` |  |
| `now` | `DateInput` |  |
| `onExpandedChange` | `((ids: string[]) => void)` |  |
| `onLoadMore` | `(() => void)` |  |
| `timeZone` | `string` |  |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `markers` | `'dot'``'avatar'` | Tone-coloured dots, or the actor’s avatar. An event `icon` wins over both. |
| `density` | `'default'``'compact'` | Compact for side panels. |
| `groupByDay` | `'true'``'false'` | Day headings, or one ordered list. |
| `ActivityEvent.tone` | `'neutral'``'accent'``'info'``'success'``'warning'``'danger'` | Marker colour for stages such as resolved or failed. Never the only signal: say it in the verb too. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Loading | – | Skeleton rows. |
| Empty | – | “No activity yet”, or the `empty` node. |
| Error | – | The `error` node replaces the feed. |
| Details open | – | The toggle reads “Hide details” with `aria-expanded="true"`. |
| More to load | – | LoadMore after the feed. |

## Interaction behaviour

- “Show details” toggles the event’s details; `onExpandedChange` reports the open ids.
- Filters are the product’s: put controls in `filters` and pass the filtered `events`.
- LoadMore calls `onLoadMore` and announces the new events.
- Timestamps show the absolute time on hover and focus (RelativeTime).

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves through filters, links in events, timestamps, detail toggles and Load more. |
| Enter / Space | Toggles details on a “Show details” button. |
| Escape | On a focused timestamp, hides its tooltip. |

## Accessibility

Role

ordered lists (one per day) labelled by their day heading

Accessible name

Each day list is labelled by its heading; an ungrouped list by

aria-label

(default “Activity”).

- Markers and icons are decorative (`aria-hidden`); the sentence carries the meaning.
- Times are `<time datetime>` with the absolute time in a tooltip on focus.
- Detail toggles use `aria-expanded` and `aria-controls`.
- Day headings default to h3; set `dayHeadingLevel` to fit the page outline.

## Responsive behaviour

- Sentences wrap; long objects break; filters wrap onto several lines.
- Nothing is hidden on narrow screens.

## Theme support

- The connector is `border-subtle`; dot tones use the status solids; bodies use a `bg-surface` card and details a `bg-subtle` panel.
- Forced colours draw markers and the connector in CanvasText.

## Do and don't

### Do

- Write verbs in the past tense and objects as links where they lead somewhere.
- Set `actor.kind` for rules, imports and integrations.
- Use `details` for field diffs and payloads; keep the sentence short.

### Don't

- Do not rely on marker colour alone for meaning.
- Do not put raw JSON in the sentence.
- Do not use it for chat: replies, reactions and grouping belong to MessageRow.

## Related components

- [AuditLogViewer](https://monarchui.dev/components/audit-log-viewer/): Security events with IP, device, export and a drawer.
- [MessageTimeline](https://monarchui.dev/components/message-timeline/): Conversations.
- [NotificationCenter](https://monarchui.dev/components/notification-center/): Unread inbox items.
- [LoadMore](https://monarchui.dev/components/load-more/): Paging the feed.
- [RelativeTime](https://monarchui.dev/components/relative-time/): Event times.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Avatar](https://monarchui.dev/components/avatar/), [RelativeTime](https://monarchui.dev/components/relative-time/), [LoadMore](https://monarchui.dev/components/load-more/), [Skeleton](https://monarchui.dev/components/skeleton/) |

## Testing coverage

- Ordered list per day labelled by its heading, with the actor-verb-object sentence and `<time datetime>`.
- Tags automation actors.
- Details disclosure by keyboard, uncontrolled and controlled.
- Filter slot, ungrouped list and avatar markers with a dot fallback.
- Loading, empty, error and load more; heading level, density and translations.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/activity-timeline; Cross-browser axe: /components/activity-timeline |

Tests: `packages/react/src/components/collaboration/activity-timeline`

## All example code

### Task activity with filters and details

```tsx
import { useState } from 'react';
import { Badge, Link, TabsFilter } from '@rookcom/monarch-react';
import { ActivityTimeline, type ActivityEvent } from '@rookcom/monarch-react/collaboration';

// PencilIcon, PlusIcon: demo icons from the docs site; use your own.

const NOW = '2026-03-02T12:00:00Z';

/** Built lazily: docs files cannot create JSX at module scope. */
const sampleEvents = (): (ActivityEvent & { type: 'change' | 'comment' })[] => [
  {
    id: 'e1',
    type: 'change',
    actor: { name: 'Ana Lopez' },
    verb: 'moved',
    object: (
      <>
        <Link href="#/components/activity-timeline">Pricing page refresh</Link> to In review
      </>
    ),
    time: '2026-03-02T11:20:00Z',
    tone: 'info',
    details: (
      <dl style={{ margin: 0, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '2px 12px' }}>
        <dt>Status</dt>
        <dd style={{ margin: 0 }}>In progress → In review</dd>
        <dt>Reviewer</dt>
        <dd style={{ margin: 0 }}>None → Ben Okafor</dd>
      </dl>
    ),
  },
  {
    id: 'e2',
    type: 'comment',
    actor: { name: 'Ben Okafor' },
    verb: 'commented',
    time: '2026-03-02T10:05:00Z',
    icon: <PencilIcon />,
    body: 'Copy looks good. Can we swap the hero image for the lighter one?',
  },
  {
    id: 'e3',
    type: 'change',
    actor: { name: 'Auto-assign rule', kind: 'automation' },
    verb: 'assigned',
    object: 'Chloé Martin',
    time: '2026-03-02T08:30:00Z',
    meta: (
      <Badge size="sm" tone="neutral">
        Rule: Round robin
      </Badge>
    ),
  },
  {
    id: 'e4',
    type: 'change',
    actor: { name: 'Dev Patel' },
    verb: 'created this task',
    time: '2026-03-01T16:45:00Z',
    icon: <PlusIcon />,
    tone: 'success',
  },
];

function FilteredDemo() {
  const [filter, setFilter] = useState('all');
  const events =
    filter === 'all' ? sampleEvents() : sampleEvents().filter((e) => e.type === filter);
  return (
    <div style={{ width: '100%', maxWidth: 560 }}>
      <ActivityTimeline
        events={events}
        defaultExpandedIds={['e1']}
        filters={
          <TabsFilter
            aria-label="Show"
            variant="pill"
            value={filter}
            onValueChange={setFilter}
            options={[
              { value: 'all', label: 'All' },
              { value: 'comment', label: 'Comments' },
              { value: 'change', label: 'Changes' },
            ]}
          />
        }
        hasMore
        onLoadMore={() => {}}
        now={NOW}
        timeZone="UTC"
      />
    </div>
  );
}

<FilteredDemo />
```

### Compact, with avatars

```tsx
import { Badge, Link } from '@rookcom/monarch-react';
import { ActivityTimeline, type ActivityEvent } from '@rookcom/monarch-react/collaboration';

// PencilIcon, PlusIcon: demo icons from the docs site; use your own.

const NOW = '2026-03-02T12:00:00Z';

/** Built lazily: docs files cannot create JSX at module scope. */
const sampleEvents = (): (ActivityEvent & { type: 'change' | 'comment' })[] => [
  {
    id: 'e1',
    type: 'change',
    actor: { name: 'Ana Lopez' },
    verb: 'moved',
    object: (
      <>
        <Link href="#/components/activity-timeline">Pricing page refresh</Link> to In review
      </>
    ),
    time: '2026-03-02T11:20:00Z',
    tone: 'info',
    details: (
      <dl style={{ margin: 0, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '2px 12px' }}>
        <dt>Status</dt>
        <dd style={{ margin: 0 }}>In progress → In review</dd>
        <dt>Reviewer</dt>
        <dd style={{ margin: 0 }}>None → Ben Okafor</dd>
      </dl>
    ),
  },
  {
    id: 'e2',
    type: 'comment',
    actor: { name: 'Ben Okafor' },
    verb: 'commented',
    time: '2026-03-02T10:05:00Z',
    icon: <PencilIcon />,
    body: 'Copy looks good. Can we swap the hero image for the lighter one?',
  },
  {
    id: 'e3',
    type: 'change',
    actor: { name: 'Auto-assign rule', kind: 'automation' },
    verb: 'assigned',
    object: 'Chloé Martin',
    time: '2026-03-02T08:30:00Z',
    meta: (
      <Badge size="sm" tone="neutral">
        Rule: Round robin
      </Badge>
    ),
  },
  {
    id: 'e4',
    type: 'change',
    actor: { name: 'Dev Patel' },
    verb: 'created this task',
    time: '2026-03-01T16:45:00Z',
    icon: <PlusIcon />,
    tone: 'success',
  },
];

<div style={{ width: '100%', maxWidth: 360 }}>
  <ActivityTimeline
    events={sampleEvents().slice(0, 3)}
    markers="avatar"
    density="compact"
    groupByDay={false}
    aria-label="Recent activity"
    now={NOW}
    timeZone="UTC"
  />
</div>
```
