Presence stackPro
Who is in a thread, people and agents together, with live state drawn as shapes and a list of what everyone is doing.
On this page
Installation
Usage
import { PresenceStack } from "@/components/ui/chatcn/pro/presence-stack"
<PresenceStack
members={[
{ id: "ines", name: "Ines Okafor", state: "here", you: true },
{ id: "ledger", name: "Ledger", kind: "agent", state: "working", activity: "Matching charge 4471" },
{ id: "noor", name: "Noor Haddad", state: "idle", since: "8 m" },
]}
onMemberSelect={(m) => follow(m.id)}
selectLabel="Follow"
/>People are circles with a photo or initials. Agents are squircles with their initials in mono, or the provider mark you pass as mark, and their names carry a quiet agent label, so the two never get confused without a robot glyph anywhere. Presence is drawn as a shape, so state reads in grayscale and forced colors: a dot is here, a ring is idle, a turning arc is an agent working and a small diamond is an agent waiting on someone. Away and offline dim the avatar instead. In the stack, being shown already means here, so plain here goes unmarked and only the states worth noticing carry a mark; the list marks everyone. The stack orders itself by urgency (waiting, working, here, idle, gone), shows max avatars and a count, and fans open a few pixels on hover or focus. The line beside it keeps the count whole and lets the names of working agents truncate first. PresenceList is the same list on its own, for a sidebar.
Examples
States
Props
| Prop | Type | Notes |
|---|---|---|
members | (CollabMember & { state, activity?, since? })[] | state: here, idle, away, offline, working, waiting |
CollabMember | { id, name, kind?, avatar?, mark?, role?, you? } | kind: "agent" draws a squircle and the agent label; mark is the provider mark you supply |
max | number | Avatars before the count. Default 4 |
size | 24 | 28 | 32 | Default 28 |
summary | boolean | The line beside the stack. Default true |
onMemberSelect, selectLabel | (member) => void, string | Rows become buttons |
open, defaultOpen, onOpenChange | boolean | The list popover |
loading, locale | boolean, string | Names join with Intl.ListFormat |
labels | Partial<PresenceStackLabels> | Every string, for translation |
Labels
labels covers the trigger name, the list title, the summary and working lines, the overflow count, the agent and you tags and every state word (labels.states). Pass translated functions for strings with counts.
Keyboard
| Key | Action |
|---|---|
| Tab | Focus the stack; it fans open |
| Enter or Space | Open the list |
| Tab inside the list | Move between members when rows are buttons |
| Esc | Close the list |
Accessibility
- The stack is one button named with the count and the summary line.
- Avatars are hidden from assistive tech; each row in the list says the name, the agent tag and the state in words.
- Presence never relies on hue: every state has its own shape and its own word.
Motion
The fan slides each avatar 3 px per index in 200 ms. Someone joining grows in from 0.96 with a fade. Reduced motion keeps the fades and drops the movement. Nothing pulses.