Skip to content
weftGitHub

Feed

A long stream of activity, with each event as a knot on a line.

SourcePackage @weft-js/warpPlayground

Preview

tsx
import { Feed } from '@weft-js/warp'export function Activity() {  return (    <Feed label="Activity" items={ACTIVITY} height="400px" />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }const DEPLOYS = [  { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' },  { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' },  { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' },  { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' },  { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' },  { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const toneOf = (status: string) => TONE_OF[status as Status]const verb: Record<string, string> = { Live: 'shipped', Building: 'started', Failed: 'broke' }const hoursAgo = (i: number) => (i < 24 ? `${i + 1}h` : `${Math.floor(i / 24)}d`)const ACTIVITY = Array.from({ length: 300 }, (_, i) => {  const d = DEPLOYS[i % 6]!  return { id: i, what: `${d.author} ${verb[d.status]} ${d.project}`, when: hoursAgo(i), detail: `${d.message}. ${d.branch} at ${d.commit}.`, tone: toneOf(d.status) }})

Installation

npm install @weft-js/warp

Usage

tsx
import { Feed } from '@weft-js/warp'<Feed label="Activity" items={ACTIVITY} height="400px" />

Loading more

onEnd asks for the next page near the end, and busy tells assistive technology.

tsx
import { state } from '@weft-js/core'import { Feed, Stack, Text } from '@weft-js/warp'export function LoadMore() {  const items = state(ACTIVITY.slice(0, 20))  const busy = state(false)  const more = async () => {    if (items.value.length >= ACTIVITY.length) return    busy.value = true    await new Promise(done => { setTimeout(done, 700) })    items.value = ACTIVITY.slice(0, items.value.length + 20)    busy.value = false  }  return (    <Stack gap="related">      <Text subtle>{items.value.length} of {ACTIVITY.length} events{busy.value ? ', loading more' : ''}.</Text>      <Feed label="Activity" items={items.value} height="340px" busy={busy.value} onEnd={() => void more()} />    </Stack>  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }const DEPLOYS = [  { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' },  { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' },  { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' },  { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' },  { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' },  { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const toneOf = (status: string) => TONE_OF[status as Status]const verb: Record<string, string> = { Live: 'shipped', Building: 'started', Failed: 'broke' }const hoursAgo = (i: number) => (i < 24 ? `${i + 1}h` : `${Math.floor(i / 24)}d`)const ACTIVITY = Array.from({ length: 300 }, (_, i) => {  const d = DEPLOYS[i % 6]!  return { id: i, what: `${d.author} ${verb[d.status]} ${d.project}`, when: hoursAgo(i), detail: `${d.message}. ${d.branch} at ${d.commit}.`, tone: toneOf(d.status) }})

Your own item

children draws the whole item, here with a coloured knot and a badge. Keep the data-parts and it stays on the thread.

tsx
import { Badge, Feed } from '@weft-js/warp'export function Custom() {  return (    <Feed label="Deploys" items={ACTIVITY} height="340px">      {item => (        <>          <span data-part="knot" style={`--_dc:var(--warp-color-${item().tone}-solid)`}></span>          <span data-part="what">{item().what}</span>          <Badge tone={item().tone}>{item().when}</Badge>          <span data-part="detail" data-measure="">{item().detail}</span>        </>      )}    </Feed>  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }const DEPLOYS = [  { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' },  { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' },  { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' },  { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' },  { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' },  { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const toneOf = (status: string) => TONE_OF[status as Status]const verb: Record<string, string> = { Live: 'shipped', Building: 'started', Failed: 'broke' }const hoursAgo = (i: number) => (i < 24 ? `${i + 1}h` : `${Math.floor(i / 24)}d`)const ACTIVITY = Array.from({ length: 300 }, (_, i) => {  const d = DEPLOYS[i % 6]!  return { id: i, what: `${d.author} ${verb[d.status]} ${d.project}`, when: hoursAgo(i), detail: `${d.message}. ${d.branch} at ${d.commit}.`, tone: toneOf(d.status) }})

When to use

Use it for

  • Audit trails and activity on a project
  • An endless stream that loads as you scroll

Reach for something else

  • VirtualList, when the rows are not events in time
  • Table, when people sort and compare

Playground

Height
import { Feed } from '@weft-js/warp'<Feed label="Activity" items={activity} height="360px" />

Accessibility

PageDown
Moves to the next event
PageUp
Moves to the previous event
↑↓
Moves between events
HomeorEnd
Goes to the first or last event
  • It follows the APG feed pattern: each event is an article inside a feed.

API Reference

PropTypeDefaultDescription
itemsreadonly T[]—Each with id and what, and optional when, detail and tone.
labelstring—Its accessible name. Fields show it; icon-only controls speak it.
heightstring'480px'The scroller's height. Default 480px.
busybooleanfalseMore is loading.
control((c: VirtualControl) => void)—Hands over a control to scroll to, focus or move between items from outside.
children((item: () => T) => unknown)—An item's whole content. Default: knot, what, when, detail.
EventTypeDefaultDescription
onEnd(() => void)—Called near the end, to load more.
onDrift((index: number, predicted: number, actual: number) => void)—Called when an item's measured height differs from the estimate, with both heights. For tuning.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="feed"] [data-part="knot"]

knotwhatwhendetail

RelatedVirtual listActivity feedTable