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.

  1. S
    Sarah Chencommented
    Love the new onboarding flow. The empty states make a huge difference. Can we ship the copy tweaks before Friday?
  2. Marcus Leementioned you in Q3 roadmap
    “@you can you own the migration checklist for this one?”
  3. Priya Nairuploaded 2 files
    Brand gradient exploration
    Brand-guidelines.pdf
    2.4 MB · PDF document
  4. Marcus Leechanged status toDone
  5. Sarah Chenreacted to your comment
  6. Priya Nairchanged the due date to Jun 20

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add activity-feed

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.

activity.tsxTSX
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>  )}
  1. Alex Riveraopened the issue
  2. Alex Riveramerged the pull request

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.

  1. Marcus Leementioned you in Q3 roadmap
  2. Priya Nairuploaded the press kit
  3. Sarah Chenclosed the milestone
Playground
Props

Cross-cutting spacing axis.

What separates one event from the next.

Applied to the first event here.

Hover, press and focus affordance.

Parts

An icon, an Avatar, or a dot.

The relative timestamp.

A muted one-liner under the header.

Trailing controls for the row.

activity.tsxTSX
<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.

header only

  1. Dana Wucommented

body only

  1. Reopened after the staging deploy went out.

card only

  1. Can we ship the copy tweaks before Friday?

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.

  1. J
    June Parkjoined the workspace
  2. June Parkwas assigned to Billing v2
  3. June Parkrenamed the project

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.

  1. Priya Nairattached 3 files
    Poster concept
    Color study
    source-assets.zip
    18 MB · Archive

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.

  1. D
    Dana Wuleft a review
    The motion polish is great, but the empty-state illustration feels a touch heavy against the cream theme. Maybe drop it to 80% opacity?

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.

  1. Marcus Leementioned you in Q3 roadmap
    Unread
  2. Billing v2is ready for review
    Unread
  3. Sarah Chenclosed the milestone

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.

  1. S
    Sarah Chencommented
    The motion polish landed nicely. One nit: the rail still reads a touch dark on cream.
  2. Priya Nairuploaded a file
    Brand-guidelines.pdf
    2.4 MB · PDF document

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.

    1. S
      Sarah Chenchecked in for the 10:30 visit
    2. Front deskmarked the invoice paid
    1. Marcus Leeleft a note on the record
      Prefers morning slots, bring the X-ray from March.
    2. Sarah Chenbooked a visit

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.

  1. June Parkedited the brief
  2. June Parkuploaded the press kit
  3. June Parkrenamed the project

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.

  1. D
    Dana Wuleft a review
    The motion polish is great, but the empty-state illustration feels a touch heavy against the cream theme. Maybe drop it to 80% opacity?

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.

comfortable

  1. Alexedited the brief
  2. Alexexported the report

compact

  1. Alexedited the brief
  2. Alexexported the report

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.

FAQ#

Use `ActivityIcon` with a tone for typed events (a status change, an upload), an `Avatar` when a specific person acted, and `ActivityDot` for low-emphasis minor events like a field edit. All three share a fixed footprint inside `ActivityMarker`, so the rail stays aligned whichever you pick.