# Presence avatars

> Who else has a record open (a channel, a comment thread's record or a document), on the record's presence topic.

Source: https://bettersupabase.com/docs/registry/ui-recipes/presence-avatars

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It calls `usePresence` from `better-supabase/react`; the [messaging block](https://bettersupabase.com/blocks/messaging) must be installed (`npx better-supabase add messaging`). Its page on bettersupabase.com is [/recipes/presence-avatars](https://bettersupabase.com/recipes/presence-avatars).

Web, with shadcn components:

```bash
npx shadcn add avatar
```

```tsx title="apps/examples/nextjs/src/features/chat/components/presence-avatars.tsx"
"use client";

import { usePresence } from "better-supabase/react";
import {
  subjectPresenceTopic,
  subjectPresenceValues,
} from "better-supabase/realtime";
import { useExtracted } from "next-intl";
import * as v from "valibot";

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
} from "@/components/ui/avatar";
import { useSession } from "@/lib/hooks";

const SHOWN = 4;

/** States that fail the schema never reach `members`. */
const viewers = subjectPresenceTopic(
  v.object({ userId: v.string(), name: v.string() }),
);

/**
 * Who else has a record open: a channel, a comment thread's record or a
 * document. The subjects module lets anyone who reads the record join its
 * presence topic. States come from the clients, so they only label avatars.
 */
export function PresenceAvatars({
  subject,
}: {
  subject: { readonly type: string; readonly id: string };
}) {
  const t = useExtracted("chat");
  const session = useSession();
  const me = session.kind === "user" ? session.user : null;

  const { members } = usePresence(
    viewers,
    subjectPresenceValues(me ? subject : null),
    { state: me ? { userId: me.id, name: me.email ?? t("Someone") } : null },
  );

  const others = new Map<string, string>();

  for (const { state } of members) {
    if (state.userId !== me?.id) {
      others.set(state.userId, state.name);
    }
  }

  if (others.size === 0) {
    return null;
  }

  const people = [...others];

  return (
    <AvatarGroup
      aria-label={t(
        "{count, plural, one {# other viewer} other {# other viewers}}",
        { count: people.length },
      )}
    >
      {people.slice(0, SHOWN).map(([userId, name]) => (
        <Avatar key={userId} size="sm" title={name}>
          <AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
        </Avatar>
      ))}
      {people.length > SHOWN ? (
        <AvatarGroupCount>+{people.length - SHOWN}</AvatarGroupCount>
      ) : null}
    </AvatarGroup>
  );
}
```