Presence list
FreeA list where an added row opens from nothing and a removed row closes, so a live feed or a queue changes without jumping. The first render is immediate.
pnpm dlx shadcn@latest add https://caliperui.site/r/presence-list.json- Q3 board pack.pdf2.4 MB
- Payroll September.csv380 KB
When to use #
Use it for
- Activity feeds, upload queues, notification lists.
- Rows added or removed while someone is looking.
Skip it when
- Search and filter results; those update at once.
- Long lists that load in pages.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/presence-list.json
Adds its 4 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 { FileText, X } from "lucide-react" import { useState } from "react" import { Button } from "@/components/ui/button" import { PresenceList } from "@/components/ui/presence-list" type Upload = { id: string; name: string; size: string }; const files = ["Q3 board pack.pdf", "Payroll September.csv", "Brand guide.fig", "Receipts.zip", "Contract — Ostra.docx", "Roadmap.key"]; export function PresenceListDemo() { const [items, setItems] = useState<Upload[]>([ { id: "0", name: files[0], size: "2.4 MB" }, { id: "1", name: files[1], size: "380 KB" }, ]); const [next, setNext] = useState(2); return ( <div className="grid w-full max-w-md gap-3"> <div className="flex gap-2"> <Button size="sm" onClick={() => { setItems((list) => [{ id: String(next), name: files[next % files.length], size: `${(next * 0.7 + 0.3).toFixed(1)} MB` }, ...list]); setNext((n) => n + 1); }}> Add file </Button> <Button size="sm" variant="secondary" disabled={!items.length} onClick={() => setItems((list) => list.slice(0, -1))}> Remove oldest </Button> </div> <PresenceList aria-label="Uploads" items={items} getKey={(item) => item.id} className="rounded-surface border border-border" itemClassName="border-b border-border last:border-b-0" renderItem={(item) => ( <div className="flex items-center gap-3 px-3 py-2.5"> <FileText aria-hidden="true" className="size-4 text-muted-foreground" /> <span className="min-w-0 flex-1 truncate text-sm">{item.name}</span> <span className="text-xs text-muted-foreground tabular-nums">{item.size}</span> <button type="button" aria-label={`Remove ${item.name}`} className="grid size-7 place-items-center rounded-control text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring" onClick={() => setItems((list) => list.filter((x) => x.id !== item.id))}> <X aria-hidden="true" className="size-3.5" /> </button> </div> )} /> {!items.length && <p className="text-sm text-muted-foreground">No uploads. Add a file to see it arrive.</p>} </div> ); }
API reference #
PresenceList
component
Also accepts the native props of its root element and a ref.
PropTypeDefault
items *readonly T[]—
getKey *(item: T) => string—
renderItem *(item: T) => ReactNode—
itemClassNamestring—
Accessibility #
It is built on native elements and keeps aria-hidden in step with state.
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.