# Activity Feed Timeline

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

Canonical: https://gsapvault.com/ui-elements/activity-feed-timeline
Live demo: https://gsapvault.com/demos/activity-feed-timeline/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Flip |
| Techniques | flip-layout, stagger, timed-stream, keyboard-navigation, svg-line-draw |
| Uses Lenis | No |

## Lighthouse, as measured

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

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### 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.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Activity Feed Timeline](https://gsapvault.com/ui-elements/activity-feed-timeline)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
