caliper uidocs

Presence list

Free

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

Installs components/ui/presence-list.tsxDepends on none beyond ReactMotion honours reduced motion
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.