ComponentsData display

Activity timeline

A record's history, newest first: who did what, with the before and after values, plus its emails, notes and meetings in one list.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

  1. Joseph A.changed Relationship from Prospect to Client
  2. Sara Lundreplied
  3. Tom Beckercreated Halcyon Bank
States
import { ActivityTimeline } from '@/ui/activity-timeline';<ActivityTimeline />

On a company's Activity tab

One line per event: the person's avatar, their name, one sentence and the time at the end. Values that have a colour show as their lozenge. Several field changes from one save fold into one line that opens. A private email shows that it exists and whose it is, never its content. The source filter above is links.

  1. Sara Lundreplied

    “Happy with the shortlist. Can we see Maya and Priya on Thursday?”

    Shared with the team · Open in Inbox

  2. Joseph A.Client meeting Quarterly review · Halcyon Bank
  3. Joseph A.changed Relationship from Prospect to Client
  4. Priya S.changed Industry and 2 other fields
    • changed Industry from Banking to Fintech
    • changed Headcount from 950 to 1,200
    • changed Fee from 15% to 18%
  5. Joseph A.added a note

    Sara wants two more data engineers by end of November. Budget is fixed at £95k; she'll flex on remote.

  6. Marcus R.emailed Sara Lund

    Gmail · 3 messages · private to Marcus until they link it

  7. Joseph A.imported Halcyon Bank from clients-oct.csv
import { LockSimpleIcon } from '@phosphor-icons/react';
import { ActivityFilter, ActivityItem, ActivityNote, ActivityTimeline, ActivityValue } from '@/ui/activity-timeline';
import { Button } from '@/ui/button';
import { Link } from '@/ui/link';

// Halcyon Bank's Activity tab, newest first. Times are relative to a fixed now so the example reads the same every day.
const now = new Date('2026-10-16T10:00:00+01:00');

export default function ActivityTimelineCompany() {
  return (
    <div className="flex w-full max-w-160 flex-col gap-200">
      <ActivityFilter
        items={[
          { label: 'All', href: '?source=all', current: true },
          { label: 'Email', href: '?source=email' },
          { label: 'Meetings', href: '?source=meetings' },
          { label: 'Echo', href: '?source=echo' },
          { label: 'Notes', href: '?source=notes' },
        ]}
      />
      <ActivityTimeline now={now} timeZone="Europe/London">
        <ActivityItem
          actor="Sara Lund"
          at="2026-10-15T16:20:00+01:00"
          body={
            <div className="flex flex-col gap-050">
              <p className="line-clamp-2 text-text">“Happy with the shortlist. Can we see Maya and Priya on Thursday?”</p>
              <p className="text-body-small leading-body-small text-text-subtle">
                Shared with the team · <Link href="?c=halcyon">Open in Inbox</Link>
              </p>
            </div>
          }
        >
          replied
        </ActivityItem>
        <ActivityItem actor="Joseph A." at="2026-10-12T09:12:00+01:00">
          Client meeting <ActivityValue>Quarterly review</ActivityValue> · Halcyon Bank
        </ActivityItem>
        <ActivityItem actor="Joseph A." at="2026-10-12T09:10:00+01:00">
          changed Relationship from <ActivityValue appearance="neutral">Prospect</ActivityValue> to <ActivityValue appearance="success">Client</ActivityValue>
        </ActivityItem>
        <ActivityItem
          actor="Priya S."
          at="2026-10-10T14:02:00+01:00"
          details={[
            <>changed Industry from <ActivityValue>Banking</ActivityValue> to <ActivityValue>Fintech</ActivityValue></>,
            <>changed Headcount from <ActivityValue>950</ActivityValue> to <ActivityValue>1,200</ActivityValue></>,
            <>changed Fee from <ActivityValue>15%</ActivityValue> to <ActivityValue>18%</ActivityValue></>,
          ]}
        >
          changed Industry and 2 other fields
        </ActivityItem>
        <ActivityItem
          actor="Joseph A."
          at="2026-10-09T11:45:00+01:00"
          body={<ActivityNote>Sara wants two more data engineers by end of November. Budget is fixed at £95k; she&apos;ll flex on remote.</ActivityNote>}
        >
          added a note
        </ActivityItem>
        <ActivityItem
          actor="Marcus R."
          at="2026-10-08T17:30:00+01:00"
          body={
            <p className="flex items-center gap-050 text-body-small leading-body-small text-text-subtle">
              <LockSimpleIcon aria-hidden className="size-3" />
              Gmail · 3 messages · private to Marcus until they link it
            </p>
          }
        >
          emailed Sara Lund
        </ActivityItem>
        <ActivityItem actor="Joseph A." at="2026-10-01T10:00:00+01:00" action={<Button size="sm" variant="ghost">Undo import</Button>}>
          imported Halcyon Bank from <ActivityValue>clients-oct.csv</ActivityValue>
        </ActivityItem>
      </ActivityTimeline>
    </div>
  );
}

Compact

On the Overview, the last few events under Recent activity, with no avatars.

  1. Sara Lund replied
  2. Joseph A.changed Relationship to Client
  3. Priya S.changed Industry and 1 other field
    • changed Industry to Fintech
    • changed Fee to 18%
import { ActivityItem, ActivityTimeline, ActivityValue } from '@/ui/activity-timeline';

const now = new Date('2026-10-16T10:00:00+01:00');

// On the Overview: the last few events under Recent activity, with no avatars.
export default function ActivityTimelineCompact() {
  return (
    <ActivityTimeline compact now={now} timeZone="Europe/London" className="w-full max-w-120">
      <ActivityItem at="2026-10-15T16:20:00+01:00">
        <span className="text-text">Sara Lund</span> replied
      </ActivityItem>
      <ActivityItem actor="Joseph A." at="2026-10-12T09:10:00+01:00">
        changed Relationship to <ActivityValue appearance="success">Client</ActivityValue>
      </ActivityItem>
      <ActivityItem actor="Priya S." at="2026-10-10T14:02:00+01:00" details={[<>changed Industry to <ActivityValue>Fintech</ActivityValue></>, <>changed Fee to <ActivityValue>18%</ActivityValue></>]}>
        changed Industry and 1 other field
      </ActivityItem>
    </ActivityTimeline>
  );
}

Times

Relative within 7 days, absolute after, in the viewer's time zone. Hover or focus a time for the exact date and time.

  1. Joseph A.moved Maya Chen to Interview
  2. Joseph A.sent the CV to Halcyon Bank
  3. Sara Lundbooked a call with Priya Shah
  4. Sara Lundadded a note
  5. Tom Beckerchanged the owner
  6. Tom Beckercreated Halcyon Bank
  7. Tom Beckerimported 1,234 companies
import { ActivityItem, ActivityTimeline } from '@/ui/activity-timeline';

const now = new Date('2026-10-16T10:00:00+01:00');

// Relative within 7 days, absolute after. Hover or focus a time for the exact date and time.
export default function ActivityTimelineTimes() {
  return (
    <ActivityTimeline now={now} timeZone="Europe/London" className="w-full max-w-120">
      <ActivityItem actor="Joseph A." at="2026-10-16T09:59:40+01:00">moved Maya Chen to Interview</ActivityItem>
      <ActivityItem actor="Joseph A." at="2026-10-16T09:35:00+01:00">sent the CV to Halcyon Bank</ActivityItem>
      <ActivityItem actor="Sara Lund" at="2026-10-16T08:02:00+01:00">booked a call with Priya Shah</ActivityItem>
      <ActivityItem actor="Sara Lund" at="2026-10-15T16:20:00+01:00">added a note</ActivityItem>
      <ActivityItem actor="Tom Becker" at="2026-10-12T09:12:00+01:00">changed the owner</ActivityItem>
      <ActivityItem actor="Tom Becker" at="2026-10-02T11:00:00+01:00">created Halcyon Bank</ActivityItem>
      <ActivityItem actor="Tom Becker" at="2025-11-20T11:00:00+00:00">imported 1,234 companies</ActivityItem>
    </ActivityTimeline>
  );
}

Long and missing content

A long note folds to three lines behind Show more. Long values and names in any script wrap. A cleared value shows as an em dash, and the value before a change is struck through.

  1. Nguyễn Thị Minh Khaiadded a note

    Spoke with Sara about the second data engineering hire. She wants someone who has run Spark jobs at scale and can mentor two juniors. Budget is fixed at £95k; she'll flex on remote but wants two days a week in the London office. Interviews would run over two weeks starting 3 November, with a take-home exercise in between. She asked us to hold off on candidates who need sponsorship until legal confirms.

  2. 王小明changed Website from halcyon-bank-international-holdings.example.com to —
import { ActivityItem, ActivityNote, ActivityTimeline, ActivityValue } from '@/ui/activity-timeline';

const now = new Date('2026-10-16T10:00:00+01:00');

// A long note folds to three lines behind Show more. Long values and names wrap; a cleared value shows an em dash.
export default function ActivityTimelineLongContent() {
  return (
    <ActivityTimeline now={now} timeZone="Europe/London" className="w-full max-w-120">
      <ActivityItem
        actor="Nguyễn Thị Minh Khai"
        at="2026-10-15T16:20:00+01:00"
        body={
          <ActivityNote>
            Spoke with Sara about the second data engineering hire. She wants someone who has run Spark jobs at scale and can mentor two juniors. Budget is fixed at £95k; she&apos;ll flex on remote but wants two days a week in the London office. Interviews would run over two weeks starting 3 November, with a take-home exercise in between. She asked us to hold off on candidates who need sponsorship until legal confirms.
          </ActivityNote>
        }
      >
        added a note
      </ActivityItem>
      <ActivityItem actor="王小明" at="2026-10-14T09:00:00+01:00">
        changed Website from <ActivityValue before>halcyon-bank-international-holdings.example.com</ActivityValue> to <ActivityValue>{null}</ActivityValue>
      </ActivityItem>
    </ActivityTimeline>
  );
}

Empty and loading

A new record, or a filter with nothing in it, shows one line with no illustration. While events load, skeleton lines hold the place.

No activity yet.

No emails with Halcyon Bank yet.

import { ActivityTimeline } from '@/ui/activity-timeline';

// A new record, or a filter with nothing in it, shows one line. While events load, skeleton lines hold the place.
export default function ActivityTimelineEmptyAndLoading() {
  return (
    <div className="grid w-full max-w-160 gap-300 sm:grid-cols-2">
      <ActivityTimeline />
      <ActivityTimeline empty="No emails with Halcyon Bank yet." />
      <ActivityTimeline loading />
    </div>
  );
}

Motion

A folded line's caret turns as it opens. Press Replay to watch it.

  1. Priya S.changed Industry and 1 other field
    • changed Industry to Fintech
    • changed Fee to 18%
import { useRef } from 'react';
import { ActivityItem, ActivityTimeline, ActivityValue } from '@/ui/activity-timeline';
import { Button } from '@/ui/button';

const now = new Date('2026-10-16T10:00:00+01:00');

// Replay opens and closes the folded line: its caret turns in 160ms; the lines appear at once.
export default function ActivityTimelineMotion() {
  const list = useRef<HTMLDivElement>(null);
  const replay = () => {
    const details = list.current?.querySelector('details');
    if (!details) return;
    details.open = true;
    setTimeout(() => (details.open = false), 900);
  };
  return (
    <div ref={list} className="flex w-full max-w-120 flex-col items-start gap-300">
      <ActivityTimeline now={now} timeZone="Europe/London" className="w-full">
        <ActivityItem actor="Priya S." at="2026-10-10T14:02:00+01:00" details={[<>changed Industry to <ActivityValue>Fintech</ActivityValue></>, <>changed Fee to <ActivityValue>18%</ActivityValue></>]}>
          changed Industry and 1 other field
        </ActivityItem>
      </ActivityTimeline>
      <Button size="sm" onClick={replay}>
        Replay
      </Button>
    </div>
  );
}

On this page