caliper uidocs

Feed stream

Free

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

Installs components/ui/feed-stream.tsxDepends on lucide-reactMotion honours reduced motion
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.