Activity Feed
A vertical timeline of what happened: comments, mentions, status changes, uploads, members joining. One rail threads every event, while each row varies its marker and its body, from a terse audit log to a rich inbox.
Installation#
Usage#
Activity Feed is composed from named parts, like Ranking. The ActivityFeed root is a semantic <ol> that owns density and the connecting rail; each ActivityItem pairs an ActivityMarker (drop an ActivityIcon, an Avatar, or an ActivityDot inside it) with an ActivityContent body. The rail is drawn automatically and closes itself on the last event.
import {
ActivityFeed,
ActivityItem,
ActivityMarker,
ActivityIcon,
ActivityContent,
ActivityHeader,
ActivityActor,
ActivityTime,
} from "@/components/ui/activity-feed"
import { GitMerge } from "@phosphor-icons/react"
export function Timeline() {
return (
<ActivityFeed>
<ActivityItem>
<ActivityMarker>
<ActivityIcon tone="success"><GitMerge /></ActivityIcon>
</ActivityMarker>
<ActivityContent>
<ActivityHeader>
<ActivityActor>Alex Rivera</ActivityActor>
merged the pull request
<ActivityTime dateTime="2026-06-15T10:30:00Z">30m ago</ActivityTime>
</ActivityHeader>
</ActivityContent>
</ActivityItem>
</ActivityFeed>
)
}Playground#
Flip the knobs and watch the snippet keep up. Note that the two groups do different things: Props are values the component takes, while Parts are children you compose in or leave out, which is why turning one off removes an element from the code rather than adding a show-something prop. Turn them all off and the event still holds its alignment.
- Marcus Leementioned you in Q3 roadmap
- Priya Nairuploaded the press kit
- Sarah Chenclosed the milestone
Cross-cutting spacing axis.
What separates one event from the next.
Applied to the first event here.
Hover, press and focus affordance.
An icon, an Avatar, or a dot.
The relative timestamp.
A muted one-liner under the header.
Trailing controls for the row.
<ActivityFeed>
<ActivityItem>
<ActivityMarker>
<ActivityIcon tone="purple">
<At weight="bold" />
</ActivityIcon>
</ActivityMarker>
<ActivityContent>
<ActivityHeader>
<ActivityActor>Marcus Lee</ActivityActor>
mentioned you in Q3 roadmap
<ActivityTime dateTime="2026-06-15T11:05:00Z">3h ago</ActivityTime>
</ActivityHeader>
</ActivityContent>
</ActivityItem>
<ActivityItem>
<ActivityMarker>
<ActivityIcon tone="teal">
<UploadSimple weight="bold" />
</ActivityIcon>
</ActivityMarker>
<ActivityContent>
<ActivityHeader>
<ActivityActor>Priya Nair</ActivityActor>
uploaded the press kit
<ActivityTime dateTime="2026-06-15T09:48:00Z">5h ago</ActivityTime>
</ActivityHeader>
</ActivityContent>
</ActivityItem>
<ActivityItem>
<ActivityMarker>
<ActivityIcon tone="success">
<CheckCircle weight="bold" />
</ActivityIcon>
</ActivityMarker>
<ActivityContent>
<ActivityHeader>
<ActivityActor>Sarah Chen</ActivityActor>
closed the milestone
<ActivityTime dateTime="2026-06-14T18:00:00Z">Yesterday</ActivityTime>
</ActivityHeader>
</ActivityContent>
</ActivityItem>
</ActivityFeed>Only the parts you need#
There is no showTime or showBody prop, because there is nothing to switch off: an event is whatever parts you put in it. Every part is droppable, and whichever one leads the content takes over the alignment, so a header-only event, a body-only event, and a card-only event all sit on the same axis as their marker.
Markers#
The marker carries the type of event. ActivityIcon is a soft tinted tile: pass a Phosphor glyph and a tone (brand, success, info, destructive, purple, and more). For people, drop an Avatar in the same slot; for minor events, ActivityDot is a low-emphasis dot. All three share a 32px footprint, so the rail stays aligned whichever you use.
Attachments#
Events carry payloads, and the feed composes them from existing components rather than re-rolling chips. Stack them in ActivityAttachments: drop a File Card row in for each file (it already ships the type-tinted icon, name, and meta) and lay ActivityImage thumbnails out in a flex flex-wrap row for an image gallery. For an upload-in-progress state, use File Card’s FileCardProgress; to collect the files in the first place, see File Upload.
Quoted comments#
ActivityCard is a nested surface for quoted content: a posted comment, a review, a changelog note. Its radius steps down concentrically from the Card the feed usually sits in. For a short, muted one-liner under the header, use ActivityBody instead.
Unread#
Mark an event as new to this reader with unread on ActivityItem: the header comes up to full-strength text and a brand dot rides the end of the row (with a screen-reader label). Deliberately no tinted row fill, since a feed is chrome-less and a band would read as a container.
Interactive events#
Pass asChild with an <a> child to make a whole event a link: the <li> stays the list item while the child becomes the hover-lit surface, so the markup is valid <li><a>. Use interactive on its own for a row that takes an onClick. The press feedback deepens the fill rather than scaling the row, the same call List makes for flush rows.
Row actions#
ActivityActions is a trailing slot for per-event controls: a reply, a download, an overflow menu. It sits beside ActivityContent rather than inside the header (which wraps), and parks on the marker’s axis so it rides the first line however tall the event grows. Don’t combine it with asChild, which would nest a control inside a link.
Day groups#
ActivityGroup gathers a run of events under a heading, a day or any other bucket, with an ActivityGroupLabel first. The label sits in the events' text column and the rail runs straight past it, so the timeline stays one line from the first event to the last. Each group is its own list, named by its label for a screen reader (“Today, list, 2 items”). The label is droppable, and a divided feed or one without the rail groups the same way.
Divided#
divided swaps the vertical rail for a hairline rule between events: the audit-log and notification-inbox reading. It re-balances the per-event rhythm so each rule sits evenly between its neighbours, and it takes the rail with it. A rule and a rail are two answers to the same question, so a divided feed never draws the connector and connector is ignored there.
Without the rail#
Set connector={false} on the feed for a plain list of events with no connecting line, useful for a single notification, a digest, or a card where the timeline framing would be too heavy.
Density#
Density is Koala’s cross-cutting spacing axis (see Density). For the feed it tunes the gap to the rail and each event’s own padding; the marker size stays fixed so the rail aligns identically at both densities. comfortable is the default; compact tightens it for dense app timelines. Set it per-feed or for a whole subtree with DensityProvider.
API reference#
ActivityFeed renders an <ol> and adds density (comfortable | compact) and divided (boolean, default false) and connector (boolean, default true, ignored when the feed is divided). ActivityItem renders an <li> and takes unread, interactive, and asChild (which implies interactive). ActivityIcon and ActivityDot take a tone (default | brand | success | warning | info | destructive | purple | pink | teal | orange). ActivityActor accepts asChild; ActivityTime renders a <time> (pass dateTime); ActivityImage requires alt. ActivityGroup renders an <li> of the feed holding its own <ol>, and ActivityGroupLabel names it. The rest ( ActivityMarker, ActivityContent, ActivityHeader, ActivityBody, ActivityCard, ActivityAttachments, ActivityActions) forward their element props. Attachments compose existing components (File Card for files). Every part accepts className, merged last.