Vanilla Breeze

activity-feed

WAI-ARIA Feed timeline of user actions with relative timestamps. Optional date grouping, optional infinite-scroll sentinel, optional avatar / icon left-rail.

Overview

<activity-feed> renders a vertical timeline of user actions ("Ada commented on issue #482, 2 hours ago"). Read-only and immutable per entry. Authors render entries as <article data-activity data-time=ISO> children; the component decorates each with a relative-time badge, optional date headings, and optional avatar / icon left-rail.

When to use which primitive

Use thisWhen
<activity-feed>One-way, immutable timeline of "actor verb object" actions with relative timestamps.
<comment-thread>Threaded, mutable discussion with replies + reactions + edit/delete.
<recently-visited>Browser navigation history, scoped per-user / per-session.
<time-index>Changelog / version-filtered release timeline.
<chat-thread>Real-time chat with sender grouping (different interaction model).

Author surface

Each entry is a direct <article data-activity> child with a required data-time attribute (ISO-8601). Inside the entry, you author the content however you like — the component does not dictate the verb vocabulary or object shape.

Date grouping

Set data-group="day" or data-group="week" to insert date headings between entries. "Today" and "Yesterday" are localized; older days use the locale's date format.

Avatar / icon left-rail

Add data-activity-avatar="URL" for a circular avatar via <user-avatar>, or data-activity-icon="name" for a Lucide icon via <icon-wc>. Mutually exclusive — avatar wins if both are set.

Infinite scroll

Set data-infinite to add an IntersectionObserver sentinel at the end of the feed. When the sentinel enters the viewport, activity-feed:load-more fires; the author appends new entries (via addEntry or direct DOM insertion) and the sentinel re-arms automatically.

Empty state

When the feed has zero entries, the component renders data-empty-text (default No recent activity). The empty state is auto-removed when the first entry is added.

Accessibility

  • Host: role="feed" per the WAI-ARIA Feed pattern.
  • Each entry: native article role with aria-posinset / aria-setsize set automatically (so AT can announce "article 3 of 50").
  • Date headings: role="heading" at aria-level="3".
  • Times: <time datetime> with the absolute timestamp in title; relative text auto-refreshes every 60s.
  • Keyboard: ArrowDown / ArrowUp move focus between entries; PageDown / PageUp jump 5 at a time; Home / End jump to first / last.
  • Reduced motion: no entrance animations.

Attributes

AttributeTypeDefaultDescription
aria-labelstringRecent activityRegion label.
data-groupstringnonenone | day | week — date headings between entries.
data-infinitebooleanfalseAdds an IntersectionObserver sentinel; emits activity-feed:load-more.
data-empty-textstringNo recent activityEmpty-state text.

Per-entry attributes (on <article data-activity>)

AttributeDescription
data-timeISO-8601 timestamp; required.
data-activity-avatarAvatar URL (renders <user-avatar>).
data-activity-iconicon-wc name (alternative to avatar).

Events

EventBubblesDetail
activity-feed:load-moreyes

JavaScript API

MethodDescription
addEntry(data, opts?)Insert an entry. data: { time, html, avatar?, icon? }; opts: { prepend? }.
removeEntry(entry)Remove a single entry element.
clear()Remove every entry (empty-state will appear).
entriesLive list of decorated entry elements.

Authors who prefer can also append <article data-activity> directly to the host — the internal MutationObserver picks up the new entry and decorates it.

See also