Feed stream
FreeA live feed that keeps your place: at the top new items slide in; scrolled away, a pill counts what is waiting and takes you back up.
pnpm dlx shadcn@latest add https://caliperui.site/r/feed-stream.json- Amara Okafor approved INV-20414 min ago
- Tomás Ferreira commented on the Ostra deal8 min ago
- Priya Raman matched 12 payments12 min ago
- Kiara Murayama invited a teammate16 min ago
- Rafael Abreu closed September20 min ago
- Amara Okafor exported the ledger24 min ago
- Tomás Ferreira changed a rule28 min ago
- Priya Raman approved INV-204132 min ago
When to use #
Use it for
- Activity, audit and support feeds that update live.
- Dashboards where events arrive while people read.
Skip it when
- Lists that change only on refresh.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/feed-stream.json
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useEffect, useState } from "react" import { Avatar } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { FeedStream } from "@/components/ui/feed-stream" type Activity = { id: string; who: string; text: string; when: string }; const actions = ["approved INV-2041", "commented on the Ostra deal", "matched 12 payments", "invited a teammate", "closed September", "exported the ledger", "changed a rule"]; const names = ["Amara Okafor", "Tomás Ferreira", "Priya Raman", "Kiara Murayama", "Rafael Abreu"]; export function FeedStreamDemo() { const [items, setItems] = useState<Activity[]>(() => Array.from({ length: 8 }, (_, index) => ({ id: `a${index}`, who: names[index % names.length]!, text: actions[index % actions.length]!, when: `${(index + 1) * 4} min ago` }))); const [live, setLive] = useState(true); useEffect(() => { if (!live) return; let n = 100; const id = setInterval(() => { n += 1; setItems((current) => [{ id: `a${n}`, who: names[n % names.length]!, text: actions[n % actions.length]!, when: "just now" }, ...current].slice(0, 40)); }, 2200); return () => clearInterval(id); }, [live]); return ( <div className="grid w-full max-w-md gap-3"> <FeedStream label="Activity" height={320} items={items} getKey={(item) => item.id} renderItem={(item) => ( <div className="flex items-center gap-3 px-4 py-3"> <Avatar name={item.who} className="size-7 rounded-[8px] text-[11px]" /> <span className="min-w-0 flex-1 text-sm"><span className="font-medium">{item.who}</span> {item.text}</span> <span className="text-xs whitespace-nowrap text-muted-foreground">{item.when}</span> </div> )} /> <Button size="sm" variant="secondary" className="w-fit" onClick={() => setLive((value) => !value)}>{live ? "Pause the feed" : "Resume the feed"}</Button> </div> ); }
API reference #
FeedStream
component
PropTypeDefaultDescription
items *readonly T[]—Newest first.
getKey *(item: T) => string—
renderItem *(item: T) => ReactNode—
label *string—Names the feed, e.g. "Activity".
heightnumber360Height of the scrolling area in pixels.
classNamestring—
Accessibility #
It exposes region and status semantics, with aria-label and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.