ui-056

Activity Feed Timeline

£5

An activity feed timeline: new events stream in at the top of a vertical rail, a filter Flip-collapses hidden kinds and times tick live.

Flip intermediate
5 more details
flip-layoutstaggertimed-streamkeyboard-navigationsvg-line-draw
Activity Feed Timeline - UI component preview

This element reads better at your own screen size than in the frame below:

About this component

The activity panel a product record carries beside its details: status changes as two small tags, comments as quoted blocks, attached files, assignee changes and mentions, each on a monochrome node on one vertical rail and grouped under Today, Yesterday and earlier days.

Read the full effect overview

When an event arrives, the rail draws down into its node, the node pops with a damped back ease, the row's content rises in, and the rows already there Flip down to make room. A radiogroup filter collapses the kinds it hides and brings them back with the same Flip, and "Show 5 older" expands the next page with a stagger. Every relative time is derived from its own <time datetime> and ticks live, and each new event is announced once through a polite live region. Light and dark themes, every colour a token.

What's included

10 items
  • New events stream in at the top: the rail draws into the node, the node pops, the content rises, and existing rows Flip down
  • Survives rapid fire: an insert that lands mid-flip records the in-flight positions, so nothing is left overlapping or holding a transform
  • All / Comments / Changes filter as a real radiogroup with a sliding indicator, arrow keys and one tab stop
  • Filtering Flip-collapses the hidden kinds out and back, with day headings joining and leaving as their rows do
  • "Show N older" reveals the next page of matching events with a stagger and hides itself when there are none
  • Relative times from real timestamps: just now, 4 min ago, 2 h ago, then the clock time under Yesterday; day headings roll over at midnight
  • Polite live region announces each new event ("New activity: Maya Chen changed status to Done")
  • add(), setFilter(), showOlder() and destroy(), plus activity:add, activity:filter and activity:older events
  • Reduced motion places every change in its final state with no draw and no Flip
  • Light and dark themes driven entirely by custom properties

Perfect for

4 use cases
  • History panels on issues, tickets, deals and orders
  • Audit trails in admin consoles and internal tools
  • Document and file activity beside a preview
  • Realtime feeds fed from a websocket or server-sent events

How it works

3 sections

One insert, four layers

add() builds the row from data (every string goes in as text, never as HTML), then records the position of every existing row, day heading and button with Flip.getState, inserts the row and Flips the rest from where they were. The new row runs its own short timeline: the rail segment scales down from its top, the node scales up with a damped back ease as the line reaches it, and the content rises and fades in a frame later.

Interruptions are the normal case

A feed gets events in bursts, and a filter can be clicked while a row is still arriving. Every layout change captures the in-flight positions first, then completes the previous Flip before starting the next, so leaving rows never stay absolutely positioned and no transform is stranded. The intro timeline animates children of the row, never the row itself, so it and Flip never fight over the same property.

Time is derived, never typed

Each row reads its <time datetime> once. A ten-second tick rewrites only the labels that changed, keeps the absolute date in the title, and regroups the feed when the date rolls over, so Today becomes Yesterday without a reload. The day groups themselves are built by the script from a flat list, which is the shape a server or an API returns.

Plugins Flip
Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, 15 September 2026. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Activity Feed Timeline opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse UI elements

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • Five event kinds out of the box: status change (old and new as two small tags), comment (avatar, name, quoted block), file attached, assignee changed, mention

  • Inserting an event: the rail segment draws down into the new node, the node pops with a damped back ease, the content rises in, and the rows already there Flip down to make room

  • Rapid fire is safe: a change that lands mid-flip records the in-flight positions and finishes the previous Flip first, so nothing is left overlapping or holding a transform

  • All / Comments / Changes filter with a sliding indicator; filtering Flip-collapses hidden rows and brings them back, and day headings follow their rows

  • "Show 5 older" reveals the next page of matching events with a stagger, relabels itself ("Show 2 older") and hides when there are none left

  • Relative times derived from each <time datetime>: "just now", "4 min ago", "2 h ago", then the clock time under Yesterday and earlier days; the absolute date sits in the title, and day headings roll over at midnight

  • A polite live region announces each new event once

  • Roles: radiogroup / radio for the filter, an ordered list per day under an h3, a labelled scroll region, role="status" for announcements

Quick Start

1. Add to your <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Copy the <section class="activity" data-activity-feed> block from index.html into your page.

Render your events as one flat list, each with a real timestamp. The script sorts them newest first and builds the day groups itself, which is the shape an API returns:

Code snippet omitted: it ships with the download.

The script looks for these hooks inside [data-activity-feed]: [data-activity-groups] (required), [data-activity-scroll], [data-activity-filter] with [role="radio"][data-value] buttons and a [data-activity-indicator], [data-activity-more], [data-activity-empty] and [data-activity-live]. Everything but the groups is optional. The rail and node spans are added for you if a row leaves them out.

Options, as attributes on the root:

Attribute Default What it does
data-visible 6 Events shown on load; the rest wait behind "Show older"
data-page 5 Events each "Show older" press reveals
data-locale en-GB Locale for day headings and the absolute date in each title

The toolbar, the "Simulate event" button, the autoplay and the script that stamps the sample events relative to the moment the page opens are demo furniture in index.html, not part of the component, as is the .stage wrapper. Leave them behind.

Pushing events

Code snippet omitted: it ships with the download.

Every field is inserted as text, never as HTML. time (a Date, timestamp or ISO string) is optional and defaults to now; an older time lands in its place in the timeline. add() returns the new <li>.

Method What it does
add(data) Inserts one event with the stream-in animation and announces it
setFilter('all' | 'comment' | 'change') Same as clicking the filter
showOlder() Reveals the next page; returns how many
refresh() Re-reads the clock now (it also ticks every 10 seconds)
destroy() Removes listeners and timers and reverts every tween

Events, dispatched on the root and bubbling: activity:add (detail.item, detail.element), activity:filter (detail.filter), activity:older (detail.count).

Code snippet omitted: it ships with the download.

Keyboard & Accessibility

Key What it does
Tab Reaches the filter (one stop), the scrollable event list, then "Show older"
Arrow Right / Arrow Down Next filter option, selected as focus moves
Arrow Left / Arrow Up Previous filter option
Home / End First / last filter option
Enter / Space Activates "Show older"
  • Roles: the filter is role="radiogroup" of role="radio" buttons with aria-checked and a roving tabindex. Each day is a section labelled by its h3, holding an ol. The scroll body is a focusable role="region" with a label, so it scrolls from the keyboard.
  • Announcements: a polite role="status" region says "New activity: Maya Chen changed status to Done" once per event, "Showing comments, 5 events" on a filter change and "5 older events shown" after a page. The node icons and rail are aria-hidden; the arrow between two tags is read as "to".
  • Time: each relative time is a <time datetime> with the absolute date and time in its title.
  • Focus: if "Show older" disappears while focused, focus moves to the event list rather than to <body>.
  • prefers-reduced-motion: every insert, filter and page lands in its final state at once. No rail draw, no node pop, no Flip.

How It Works

Inserting. add() builds the row, records every existing row, day heading and button with Flip.getState, inserts the row and hands the state to Flip.from, so the old rows travel down from where they were. The new row runs its own timeline on its children: the rail segment scales from its top, the node scales up with back.out(1.5) as the line reaches it, and the content fades and rises a frame later. Because the intro never animates the row itself, it and Flip never write the same transform.

Interruptions. Every layout change (insert, filter, show older) goes through one function that records the in-flight positions first and then completes the previous Flip with progress(1) before starting the next. That is what keeps a leaving row from being stranded with an absolute position when a filter is clicked mid-insert, and why five events in a burst settle into a clean list.

Filtering and paging. One function decides visibility for everything: rows (revealed and matching the filter), day sections (any visible row), the end of each day's rail, the newest node, the "Show older" label and the empty line. Flip's onEnter and onLeave fade the rows that join and leave, with a stagger for a page of older events.

Time. Each row reads its datetime once. A ten-second tick rewrites only labels that changed and regroups the feed when the date rolls over, so Today becomes Yesterday without a reload.

Themes

Two themes, light and dark (the demo default), the same design at two sets of token values. Set the attribute on <body>, or on the .activity root in your own page:

Code snippet omitted: it ships with the download.

Every colour, radius and shadow is a custom property in the [data-variant="light"] and [data-variant="dark"] blocks at the top of assets/style.css, and nothing in assets/script.js reads the theme name. To match your product, re-value:

  • --raised, --inset, --hover: the panel, the comment block, avatar and hover fills
  • --ink, --ink-2, --ink-3: names, body copy, times and labels
  • --line, --line-strong, --rail: hairlines, tag borders and the rail
  • --accent, --accent-ink, --accent-text, --accent-wash: the newest node, the active filter and the focus ring
  • --st-backlog, --st-todo, --st-progress, --st-review, --st-done: the status dots
  • --activity-width, --activity-height, --radius, --radius-sm, --radius-xs, --font

--ground and --ground-2 colour the demo page and the filter track. Keep text colours solid rather than translucent, so contrast stays measurable.

Customisation

  • New event kind: add it to KIND_GROUP at the top of assets/script.js with the filter it belongs to, give it a branch in buildItem, and an icon in style.css as .activity__item[data-kind="yours"] { --af-icon: url(...) }.
  • New status: add its label to STATUS_KEYS and a dot colour as .activity__tag[data-status="yours"]::before.
  • Relative time wording is relative() in assets/script.js; the tick interval is TICK_MS.
  • Insert timings are the intro() timeline; Flip durations are in animateLayout().

Requirements

  • GSAP 3.12+ with the Flip plugin
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

The Vault library, plus future additions to the library.