shadcn.io is not affiliated with official shadcn/ui
Activity Feed Timeline Block for Radix UI
A team activity feed on a vertical rail: eleven events by five people under day heads, merged into nodes by a Merge events within select, with dashed rail and a quiet-time label wherever more than an hour passed.
Installation
Overview
Show what a team did, and when it went quiet, with this activity feed timeline for React with Radix UI. It is written for Teamweft, a fictional team workspace. A hairline frame with a plus mark on each corner holds one vertical rail with a node for every stretch of activity, newest first, under day heads ("Today, Wed 14 Oct"). A Native Select in the header, "Merge events within", offers Off, 5 min, 30 min and 2 h: the nodes are derived again from the same eleven events, and events that follow one another within that time become one node ("Ava Patel and 2 others, 4 events") that opens in place to its own lines with their clocks. Where two nodes of one day are more than an hour apart, the rail turns dashed and a label says how long it was quiet. Each single event has a round tile with an icon for what happened (opened, pushed, commented, approved, merged, deployed) and the person and the thing in words; a merged node shows its event count. A status line below the rail says how many events sit in how many nodes. The panel keeps the same height for every setting. It is built from Native Select, Guides and lucide icons, with a CSS entrance and your theme's tokens for every colour. For team workspaces, project pages and anywhere people ask what changed while they were away. The events are example data.
FAQ
How do I change the events?
Edit EVENTS at the top of the file, oldest first. Each event has an id, an ISO time at in UTC, an actor, a target and a verb, which is one of six constants that pair an icon with a word (OPENED, PUSHED, COMMENTED, APPROVED, MERGED, DEPLOYED). Add a verb by adding one more constant. Everything else is derived from those times: the day heads, the nodes, the quiet gaps and the counts, so nothing is typed twice.
How does the merging work?
cluster() walks the events from oldest to newest and joins an event to the node before it when it falls on the same day and within the chosen number of minutes of that node's last event, so a burst chains together even when it is longer than the window. Off keeps every event as its own node. A merged node is named after whoever did most of it, and it opens with its button to show each event and its clock.
What counts as a quiet gap?
More than 60 minutes between the last event of one node and the first of the next one on the same day, set by QUIET_AFTER. The segment of the rail between them is dashed and the label under the node names the span ("2 h 33 min quiet"). A gap that crosses midnight is not labelled: the day head marks it.
What stays in the page, and how do I use real events?
Everything: the select and the open nodes are local state, and no request is made. To use real data, pass your own events in the same shape, with times as ISO strings in UTC, and keep the helpers: they use no time library and no locale, so the server and every browser write the same text. If your feed is long, fetch it a page at a time and run cluster() on what you have.
Is it accessible?
The feed is an ordered list and each day head is a heading. A merged node is a real button with aria-expanded, named by its text, and its events are hidden from the keyboard and from screen readers while it is closed. Nothing relies on colour: the type of an event is an icon and a word. The status line is a polite live region that is there from load and tells a screen reader the new counts when the setting changes.