X Data

Star on GitHubFeedback
X DataNative HTMLot-feed

Activity feed

A history of what happened, grouped by day: who, what, and a dotted leader to when, on one hairline down the margin. The newest carries the accent; the rest waits behind "and 4 more".

Report an issue with Activity feed

Example

Ledger

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg
  • The proof for the ferry office went out
  • Chiara Onofri moved Signage to review

Yesterday

  • Ada Lindqvist left a note on the wordmarkThe k and the r touch below eight point.
and 3 moreHide these 3
  • Bruno Maček set the timetable in Archivo

Saturday 26 September

  • Two people joined the project
  • Chiara Onofri started Harbour identity
Almanac

30September2026Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg
  • The proof for the ferry office went out
  • Chiara Onofri moved Signage to review

29September2026Yesterday

  • Ada Lindqvist left a note on the wordmarkThe k and the r touch below eight point.
  • Bruno Maček set the timetable in Archivo
and 2 moreHide these 2

26September2026Sat

  • Two people joined the project
  • Chiara Onofri started Harbour identity
Lapse

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg

5 hours earlier

  • The proof for the ferry office went out

An hour earlier

  • Chiara Onofri moved Signage to review

Yesterday

  • Ada Lindqvist left a note on the wordmarkThe k and the r touch below eight point.

4 hours earlier

  • Bruno Maček set the timetable in Archivo
and 2 moreHide these 2

Saturday 26 September

  • Two people joined the project
  • Chiara Onofri started Harbour identity
Newest

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg
  • The proof for the ferry office went out
Pointed at

Today

  • Bruno Maček uploaded harbour-mono.svg
New since you looked

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg

Where you left off

  • The proof for the ferry office went out
The rest, folded

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
and 2 moreHide these 2
  • Bruno Maček uploaded harbour-mono.svg
  • The proof for the ferry office went out
Right to left

Today

  • Ada Lindqvist approved the harbour markIt holds at stamp size. Send it to the ferry office.
  • Bruno Maček uploaded harbour-mono.svg
  • The proof for the ferry office went out
Nothing yet

Nothing has happened yet.

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, avatar, collapsible, item, and marker along.

npx shadcn@latest add https://thedirectors.agency/ui/r/activity-feed.json

Usage

activity-feed-example.tsx
import { ActivityFeed, type ActivityEntry } from "@/components/ui/activity-feed"const entries: ActivityEntry[] = [{ id: "a1", who: "Ada Lindqvist", what: <>approved <i className="ot-yours">the harbour mark</i></>, detail: "It holds at stamp size. Send it to the ferry office.", at: "2026-09-30T16:40" },{ id: "a2", who: "Bruno Maček", what: <>uploaded <i className="ot-yours">harbour-mono.svg</i></>, at: "2026-09-30T16:12" },{ id: "a3", what: "The proof for the ferry office went out", at: "2026-09-30T11:05" },{ id: "a4", who: "Chiara Onofri", what: <>moved <i className="ot-yours">Signage</i> to review</>, at: "2026-09-30T09:52" },{ id: "a5", who: "Ada Lindqvist", what: <>left a note on <i className="ot-yours">the wordmark</i></>, detail: "The k and the r touch below eight point.", at: "2026-09-29T18:20" },{ id: "a6", who: "Bruno Maček", what: <>set <i className="ot-yours">the timetable</i> in Archivo</>, at: "2026-09-29T14:03" },{ id: "a7", what: "Two people joined the project", at: "2026-09-26T10:30" },{ id: "a8", who: "Chiara Onofri", what: <>started <i className="ot-yours">Harbour identity</i></>, at: "2026-09-26T10:02" },]const days = { "2026-09-30": "Today", "2026-09-29": "Yesterday" }export default function Example() {return (<div className="grid w-full grid-cols-[minmax(0,1fr)] justify-items-stretch gap-16"><div className="grid w-full max-w-[40rem] gap-3"><span className="ot-label">Ledger</span><ActivityFeed entries={entries} days={days} initial={5} /></div><div className="grid w-full max-w-[40rem] gap-3"><span className="ot-label">Almanac</span><ActivityFeed variant="almanac" entries={entries} days={days} initial={6} /></div><div className="grid w-full max-w-[40rem] gap-3"><span className="ot-label">Lapse</span><ActivityFeed variant="lapse" entries={entries} days={days} initial={6} /></div></div>)}const wide = { width: "26rem", maxWidth: "100%" }const few = entries.slice(0, 3)

Contract

  • Underneath: native, composed of item, avatar, marker and collapsible.
  • Anatomy: <section class="ot-feed" data-variant="ledger | almanac | lapse" aria-label> holding day markers and ul.ot-items.ot-feed-run of li.ot-item.ot-feed-entry (data-newest on the first when nothing is new). Each entry: .ot-feed-bead (aria-hidden: an s avatar, or .ot-feed-dot for what the system did), the title (b.ot-feed-who, then what happened, the object in .ot-yours), an optional detail, the leader, and <time> at the end. Past initial, the rest waits in a details.ot-feed-more whose summary says "and 4 more"; fresh puts a ribbon marker ("Where you left off") before the first entry you've seen. at is read as written ("2026-09-30T09:52") and formatted in UTC, so the server and the page agree.
  • A history set as a ledger, newest first: who in ink, what they did in graphite, a dotted leader to the time. One hairline runs down the margin through each stretch of entries, starting and stopping at the beads on the first lines; the newest bead is the one accent (the ring of its avatar, or its dot). No cards, no icons: a person is their initial, the system a full stop.
  • Ledger (the default), after contents pages: days are divider markers ("Today", "Yesterday", or the date), each day's entries hung on its own thread.
  • Almanac, after the "Less but better" calendar: each day opens on its date set huge and heavy (--ot-f, 700, tight), the month over the year in small capitals beside it and the weekday (or days label) at the far end, the way the calendar sets a leaf.
  • Lapse, after Principle 1 and the marker's lapse: the time between entries is given as silence. A quiet of an hour or more breaks the thread with a lapse marker ("5 hours earlier") whose room grows with how long it went quiet; a new day is a lapse named for the day.
  • Pointing at an entry inks its leader and its time (the item's move). Opening the rest, its entries arrive down the line in turn. Right to left the thread and beads move to the right. Forced colours: the thread and dots are CanvasText, the newest Highlight.

Props

Props of Activity feed
PropTypeDefaultWhat it does
entriesActivityEntry[]Newest first; the feed never reorders them. Each is { id, who?, what, detail?, at, time? }. who names the avatar and leads the sentence (leave it out for what the system did, and a dot stands in); what is the rest of the sentence; at is local time written out ("2026-09-30T09:52"), read as written so the server and the browser agree.
variant"ledger" | "almanac" | "lapse""ledger"ledger: each day begins at a divider. almanac: each day begins with its date set as the calendar poster sets it, the day huge and heavy, month over year beside it, the weekday at the far end. lapse: no rules; the silence between two entries grows with the time between them (an hour or more), and a new day is said inside its silence.
initialnumberHow many show before the rest folds behind "and 4 more". Defaults to all.
freshnumber0How many at the top are new since the person last looked. The ribbon is laid under them and takes the accent from the newest.
daysRecord<string, ReactNode>Names for days, by date: { "2026-09-30": "Today" }. Other days are written out in the locale.
localestring"en-GB"For the written-out days.
labelstring"Activity"Names the feed for assistive technology.
more / less(count: number) => ReactNodeThe control's words, closed and open: "and 4 more", "Hide these 4".
lapse(minutes: number) => ReactNodeLapse only: the words for a pause within a day ("3 hours earlier").
seenReactNode"Where you left off"The ribbon's words.
emptyReactNode"Nothing has happened yet."Said when there are no entries.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Activity feedContents pages; "Less but better" calendarA ledger of what happened: who, what, a leader to when; one hairline threads the beads, the newest in the accent
Almanac activity feed"Less but better" calendarEach day opens on its date set huge and heavy, month and year beside it
Lapse activity feedPrinciple 1; the marker's lapseThe quiet between entries is given as silence that grows with the gap

How it moves

How it moves
ComponentArticulationWhat moves
Activity feedArpeggioOpened, the rest arrive down the line in turn; pointing inks an entry's leader and time