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/warpUsage
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
articleinside afeed.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
items | readonly T[] | — | Each with id and what, and optional when, detail and tone. |
label | string | — | Its accessible name. Fields show it; icon-only controls speak it. |
height | string | '480px' | The scroller's height. Default 480px. |
busy | boolean | false | More 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. |
| Event | Type | Default | Description |
|---|---|---|---|
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