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 feedExample
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.jsonUsage
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,markerandcollapsible. - Anatomy:
<section class="ot-feed" data-variant="ledger | almanac | lapse" aria-label>holding day markers andul.ot-items.ot-feed-runofli.ot-item.ot-feed-entry(data-neweston the first when nothing is new). Each entry:.ot-feed-bead(aria-hidden: ansavatar, or.ot-feed-dotfor 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. Pastinitial, the rest waits in adetails.ot-feed-morewhose summary says "and 4 more";freshputs a ribbon marker ("Where you left off") before the first entry you've seen.atis 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 (ordayslabel) 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
| Prop | Type | Default | What it does |
|---|---|---|---|
entries | ActivityEntry[] | 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. |
initial | number | How many show before the rest folds behind "and 4 more". Defaults to all. | |
fresh | number | 0 | How many at the top are new since the person last looked. The ribbon is laid under them and takes the accent from the newest. |
days | Record<string, ReactNode> | Names for days, by date: { "2026-09-30": "Today" }. Other days are written out in the locale. | |
locale | string | "en-GB" | For the written-out days. |
label | string | "Activity" | Names the feed for assistive technology. |
more / less | (count: number) => ReactNode | The control's words, closed and open: "and 4 more", "Hide these 4". | |
lapse | (minutes: number) => ReactNode | Lapse only: the words for a pause within a day ("3 hours earlier"). | |
seen | ReactNode | "Where you left off" | The ribbon's words. |
empty | ReactNode | "Nothing has happened yet." | Said when there are no entries. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Activity feed | Contents pages; "Less but better" calendar | A 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" calendar | Each day opens on its date set huge and heavy, month and year beside it |
| Lapse activity feed | Principle 1; the marker's lapse | The quiet between entries is given as silence that grows with the gap |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Activity feed | Arpeggio | Opened, the rest arrive down the line in turn; pointing inks an entry's leader and time |