Xzero UI
Pro library

Presence stackPro

Avatars of the people here now: joins pop in as the stack reflows, a counter rolls past the limit, dots show typing and idle, and hover fans it out with a gliding name label.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/presence-stack
Live · keyboard ready

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/presence-stack

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { PresenceStack, type PresencePerson } from "@/components/xzero/presence-stack/presence-stack";

export function DocumentHeader({ doc, me }: { doc: Doc; me: User }) {
  // From your realtime layer: Liveblocks, PartyKit, Supabase Realtime or your own socket.
  const others = useOthers();
  const people: PresencePerson[] = [
    { id: me.id, name: me.name, src: me.avatar, status: "active" },
    ...others.map(other => ({
      id: other.id,
      name: other.name,
      src: other.avatar,
      status: other.typing ? "typing" : other.idle ? "idle" : "active",
      detail: other.section ? `Editing ${other.section}` : undefined,
    })),
  ];

  return (
    <header className="flex items-center justify-between">
      <h1>{doc.title}</h1>
      <PresenceStack people={people} selfId={me.id} align="end" onPersonSelect={person => followCursor(person.id)} />
    </header>
  );
}

API reference

PresenceStack

Overlapping avatars of the people here now. A person joining pops into their slot on a spring while the others slide to make room; someone leaving shrinks away and the gap closes. Past max the rest collapse into a +N counter that rolls up or down. Each face carries a status: a green dot for active, a dimmed portrait with a grey dot for idle, and a dot that stretches into a tiny typing indicator while they type. Hover or keyboard focus fans the stack out and one name label glides between the faces, its width following each name on a spring. Clicking opens a list of everyone with what they are doing.

PropTypeDefaultDescription
people (required)readonly PresencePerson[]–Everyone here now, in display order: { id, name, src?, status?: "active" | "idle" | "typing", detail? }. Add, remove, reorder or change a status and the stack animates to match. Ids key the avatars, so the same person keeps their place.
maxnumber4Faces shown before the rest collapse into the +N counter.
size"sm" | "md" | "lg""md"Avatar diameter: 28, 36 or 44px. The overlap, status dot and label scale with it.
align"start" | "end""start"The side the stack is anchored to. The component reserves the width of a full stack; with end the faces sit against the right edge and grow and fan to the left, which suits a header next to a Share button.
selfIdstring–Id of the person using the app. Marked (you) in the hover label and the list.
labelstring"People here now"Accessible name of the stack button. The count of people is added to it.
headingstring"Here now"Heading of the list, shown with a rolling count.
openboolean–Whether the list of everyone is open. Pass with onOpenChange to control it.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void–Called when the list opens or closes, from a click, Escape or a click outside.
onPersonSelect(person: PresencePerson) => void–Makes each row in the list a button, for example to follow that person's cursor or jump to where they are. The list closes after a pick.
announcebooleantrueAnnounces joins and leaves in a polite live region, such as Emma Collins joined.
classNamestring–Class on the root element.
styleCSSProperties–Inline styles on the root element. Set --presence-ring to the color behind the stack so the cut out rings match it.

Keyboard interactions

KeysAction
TabFocuses the stack. Keyboard focus fans it out so every face is visible.
Arrow Left / Arrow RightMoves the name label from face to face while the stack has focus.
Enter / SpaceOpens the list of everyone; focus moves into it.
Tab (in the list)Moves between rows when onPersonSelect makes them buttons.
EscapeCloses the list and returns focus to the stack.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Also in avatars

Guidance for AI tools

Presence stack: Avatars of the people here now: joins pop in as the stack reflows, a counter rolls past the limit, dots show typing and idle, and hover fans it out with a gliding name label. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components