PresenceBar

<PresenceBar> shows a stack of avatars representing other users currently viewing or editing the same record. By default it uses the browser's BroadcastChannel API for same-origin cross-tab presence, so it works in dev/demo without backend setup. For production, plug in a WebSocket / Supabase Realtime / SSE transport via the transport prop.

Installation

pnpm dlx shadcn@latest add @shadmin/presence-bar

Usage

import { Show, ShowActions, PresenceBar } from "@/components/admin";
 
const ProductShow = () => (
  <Show
    actions={
      <ShowActions>
        <PresenceBar />
      </ShowActions>
    }
  >
    {/* ... */}
  </Show>
);

When no topic is set, it derives the topic from the resource + record id: presence/{resource}/{id}.

Props

PropTypeDefaultDescription
topicstringderived from record contextPub/sub channel name.
currentUser{ id; name; avatar? }from useGetIdentity()Who I am.
maxAvatarsnumber5Cap before showing +N more.
heartbeatMsnumber15000How often to re-broadcast presence.
staleMsnumber30000Drop users without a heartbeat after this many ms.
transportPresenceTransportBroadcastChannelPlug in a custom transport.

Custom transport

interface PresenceTransport {
  subscribe: (
    topic: string,
    handler: (state: PresenceState) => void,
  ) => () => void;
  publish: (topic: string, state: PresenceState) => void;
}
 
interface PresenceState {
  user: { id: string; name: string; avatar?: string };
  timestamp: number;
}

Example WebSocket adapter:

const wsTransport: PresenceTransport = {
  subscribe: (topic, handler) => {
    const ws = new WebSocket(`wss://realtime.example.com/${topic}`);
    ws.addEventListener("message", (ev) => handler(JSON.parse(ev.data)));
    return () => ws.close();
  },
  publish: (topic, state) => {
    /* send to your realtime endpoint */
  },
};

Notes

  • BroadcastChannel only works for same-origin tabs in the same browser. Use a network transport for cross-machine presence.
  • The component renders nothing when no other users are present, so it's safe to embed unconditionally.