Composables

useRealtimePresence

Track who else is present in a room.

useRealtimePresence

Track which clients are currently present in a room, with optional info about each one (a name, avatar, cursor position). Useful for "who's viewing this page" indicators, avatar stacks, and online lists.

Usage

<script setup lang="ts">
const { members, join, leave } = useRealtimePresence('document:123', {
  info: { name: 'Alice', avatarUrl: '/alice.png' },
})
</script>

<template>
  <UAvatarGroup>
    <UAvatar
      v-for="(info, connectionId) in members"
      :key="connectionId"
      :src="info.avatarUrl"
      :text="info.name[0]"
    />
  </UAvatarGroup>
</template>

Type Signature

function useRealtimePresence<TInfo = unknown>(
  room: string,
  options?: UseRealtimePresenceOptions<TInfo>
): UseRealtimePresenceReturn<TInfo>

Parameters

room

  • Type: string
  • Required: Yes

An opaque group tag. All clients calling useRealtimePresence with the same room string see each other. It's just a string key, not tied to routes or URLs, so you can scope it however makes sense (a document id, a page path, a chat channel).

options

  • Type: UseRealtimePresenceOptions<TInfo>
  • Required: No
interface UseRealtimePresenceOptions<TInfo> {
  /**
   * Opaque data shown to other room members (e.g. `{ name, avatarUrl }`). Given up front, this
   * client auto-joins the room on setup; omit it to only observe who else is present.
   */
  info?: TInfo
}

Passing info joins the room immediately as a member. Omitting it lets you observe who's present without appearing in members yourself, useful for a spectator view.

Return Value

interface UseRealtimePresenceReturn<TInfo> {
  members: Readonly<Ref<Record<string, TInfo>>>
  join: () => Promise<void>
  leave: () => Promise<void>
}

members

A readonly ref keyed by connection id, with the info each member joined with:

const { members } = useRealtimePresence('document:123')

watchEffect(() => {
  console.log(`${Object.keys(members.value).length} people here`)
})

join()

Joins the room using the info passed to the composable. Called automatically if info was provided; call it manually if you joined without info and want to become a member later:

const { join } = useRealtimePresence('document:123')

function startEditing() {
  join()
}

leave()

Leaves the room. Also called automatically on unmount:

const { leave } = useRealtimePresence('document:123')

function stopEditing() {
  leave()
}

Examples

Who's Viewing This Page

Show an avatar stack of everyone currently viewing:

ViewerAvatars.vue
<script setup lang="ts">
const props = defineProps<{ documentId: string }>()
const currentUser = useCurrentUser()

const { members } = useRealtimePresence(`document:${props.documentId}`, {
  info: { name: currentUser.name, avatarUrl: currentUser.avatarUrl },
})
</script>

<template>
  <UAvatarGroup :max="5">
    <UAvatar
      v-for="(info, connectionId) in members"
      :key="connectionId"
      :src="info.avatarUrl"
      :alt="info.name"
      :text="info.name[0]"
    />
  </UAvatarGroup>
</template>

Read-Only Viewer Count

Observe the room without joining it, useful for a dashboard that shows activity without appearing as a "viewer" itself:

ViewerCount.vue
<script setup lang="ts">
const props = defineProps<{ documentId: string }>()

// No `info` passed: this client observes but doesn't join
const { members } = useRealtimePresence(`document:${props.documentId}`)
</script>

<template>
  <UBadge color="neutral" variant="subtle" icon="i-lucide-eye">
    {{ Object.keys(members).length }} {{ Object.keys(members).length === 1 ? 'person' : 'people' }} currently viewing
  </UBadge>
</template>