Sign in

Presence

Presence is temporary state associated with each connected user in a room, updating in realtime and disappearing when the connection ends. Use Presence for creating live avatars, realtime cursors, multiplayer selections, typing indicators, and other temporary UI elements that do not belong in the persisted document. You can also set Presence on the server, enabling AI agent Presence in your app.

Ready-made components

If you’d like to get started quickly, Liveblocks provides basic ready-made components for Presence, Cursors and AvatarStack, which correspondingly display realtime cursors and avatars.

import { AvatarStack, Cursors } from "@liveblocks/react-ui";
function App() { return ( <div> <Cursors /> <AvatarStack /> </div> );}

Follow the Presence quickstart guide to set them up.

Custom Presence

Using Liveblocks React hooks, you can build any sort of realtime UI Presence into your app. Presence is represented by a JSON object, and is updated in realtime for every connected user. Before you get started, decide on the shape of your Presence object, and set it in your config file.

For example, live cursors will use x and y coordinates, whereas a typing indicator will use a boolean value. Set this in your config file.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {      cursor: { x: number; y: number } | null;    };  }}

Next, set an initial value for your Presence in RoomProvider. In this snippet, null represents a cursor that is offscreen.

import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return ( <RoomProvider initialPresence={{ cursor: null }}> {/* ... */} </RoomProvider> );}

Set up user info

When using Presence, it's often useful to pass in user info from your authentication system, such as a name, avatar, and color, to be displayed in the UI. This is helpful if you’re rendering UI such as a user avatar or name tag, static information that won’t update in realtime.

To do this, first, set your types in liveblocks.config.ts. For example, if each user has the properties we just described.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {      cursor: { x: number; y: number } | null;    };
UserInfo: { name: string; color: string; avatar: string; }; }}

Next, when authenticating Liveblocks, pass corresponding user info from your auth system to userInfo inside your authentication endpoint. Any string, number, or boolean property can be passed in.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!,});
export async function POST(request: Request) { const user = ();
const { status, body } = await liveblocks.identifyUser( { userId: user.id }, { userInfo: { name: user.name, color: user.color, avatar: user.avatar, }, } );
return new Response(body, { status });}

Setting user Presence

useUpdateMyPresence allows you to update the current user’s Presence. Use this, for example, to set a user’s cursor position on the page, or set a typing indicator.

import { useUpdateMyPresence } from "@liveblocks/react/suspense";
function CursorOverlay() { const updateMyPresence = useUpdateMyPresence();
return ( <div style={{ width: "100vw", height: "100vh" }} onPointerMove={ (event) => updateMyPresence({ cursor: { x: event.clientX, y: event.clientY } }) } onPointerLeave={ () => updateMyPresence({ cursor: null }) } /> );}

Using others' Presence

useOthers returns a list of every connected users in the room. For example, you can render each user’s cursor using their presence.cursor values, passing in their user info from before to show their name and color.

import { useOthers } from "@liveblocks/react/suspense";import { Cursor } from "@liveblocks/react-ui";
function Cursors() { const others = useOthers();
return ( <div style={{ position: "relative", width: "100vw", height: "100vh" }}> {others.map(({ connectionId, presence, info }) => presence.cursor ? ( <Cursor key={connectionId} label={info.name} color={info.color} style={{ position: "absolute", top: 0, left: 0, transform: `translate(${presence.cursor.x}px, ${presence.cursor.y}px)`, }} /> ) : null )}{" "} </div> );}

Avatar stack

After adding user info, you can render a live avatar stack wuth useOthers and your info.avatar property.

import { useOthers } from "@liveblocks/react/suspense";
function AvatarStack() { const others = useOthers();
return ( <div style={{ display: "flex", marginLeft: 4 }}> {others.map(({ connectionId, presence, info }) => ( <img key={connectionId} src={info.avatar} alt={info.name} style={{ width: 20, height: 20, borderRadius: "50%", marginLeft: -4 }} /> ))} </div> );}

Typing indicator

To create a typing indicator, use useUpdateMyPresence to set typing to true on input, then clear it with a timeout after the user stops typing.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {      typing: boolean;    };  }}
import { useRef } from "react";import { useUpdateMyPresence } from "@liveblocks/react/suspense";
function TypingIndicator() { const updateMyPresence = useUpdateMyPresence(); const timeoutId = useRef<number>();
return ( <input type="text" onInput={() => { updateMyPresence({ typing: true }); window.clearTimeout(timeoutId.current);
timeoutId.current = window.setTimeout(() => { updateMyPresence({ typing: false }); }, 1000); }} /> );}

Selection indicator

To create a selection indicator, use useUpdateMyPresence to set selection on an item, then add an outline when the item is selected. In this example, multiple textareas are rendered, and when one is selected by a user, an outline appears around it.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {      selection: string | null;    };  }}
import { useUpdateMyPresence } from "@liveblocks/react/suspense";
function Textarea({ id }: { id: string }) { const updateMyPresence = useUpdateMyPresence(); const useOthers = useOthers(); const isSelected = others.find(({ presence }) => presence.selection === id);
return ( <textarea id={id} onSelect={() => updateMyPresence({ selection: id })} onBlur={() => updateMyPresence({ selection: null })} style={{ outline: isSelected ? "2px solid #7c3aed" : "none", }} /> );}
function Textareas() { return ( <div> <Textarea id="textarea-1" /> <Textarea id="textarea-2" /> </div> );}

Server-side Presence

As well as on the client, you can set Presence on the server, especially helful for enabling AI agent Presence in your app. The Liveblocks.setPresence method is used for this, and after it’s called, useOthers will return the agent in the list of connected users. data and userInfo correspond to Presence and UserInfo in your types.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {      cursor: { x: number; y: number } | null;    };    UserInfo: {      name: string;      color: string;      avatar: string;    };  }}
await liveblocks.setPresence("my-room-id", {  userId: "agent-123",  data: {    cursor: { x: 100, y: 200 },  },  userInfo: {    name: "AI agent",    color: "#7c3aed",    avatar: "https://example.com/ai-agent.png",  },  ttl: 30,});

Presence expires after the time-to-live (TTL) period, in seconds, and is removed from the list of connected users. To remove agent Presence, call it with ttl: 2, the minimum value, and it will be removed shortly after.

Example usage

One way to use agent Presence is to set it before running an AI workflow, then remove it after the workflow is complete. Here’s an example that modifies a Sync document, displaying an AI avatar as it does the work. Avatars don’t need any Presence data, so we can set it to an empty object.

liveblocks.config.ts
declare global {  interface Liveblocks {    Presence: {};    UserInfo: {      name: string;      color: string;      avatar: string;    };  }}

Set Presence before and after running the AI workflow, setting ttl to the minimum value at the end.

import { Liveblocks } from "@liveblocks/node";import { generateText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!,});
await liveblocks.setPresence("my-room-id", { userId: "agent-123", data: {}, userInfo: { name: "AI agent", color: "#7c3aed", avatar: "https://example.com/ai-agent.png", }, ttl: 30,});
await liveblocks.mutateStorage("my-room-id", async ({ root }) => { const document = root.toJSON();
const { output } = await generateText({ model: "openai/gpt-5.6-sol", output: Output.object({ schema: z.object({ content: z.string(), }), }), prompt: `Write a paragraph about realtime collaboration. Here is the document: ${document}`, });
root.get("document").set("content", output.content);});
await liveblocks.setPresence("my-room-id", { userId: "agent-123", data: {}, userInfo: { name: "AI agent", color: "#7c3aed", avatar: "https://example.com/ai-agent.png", }, ttl: 2,});

In the front end, agent-123 will appear in the list of connected users, and the AI avatar will be displayed in the UI.

import { useOthers } from "@liveblocks/react/suspense";
function AvatarStack() { const others = useOthers();
return ( <div style={{ display: "flex", marginLeft: 4 }}> {others.map(({ connectionId, presence, info }) => ( <img key={connectionId} src={info.avatar} alt={info.name} style={{ width: 20, height: 20, borderRadius: "50%", marginLeft: -4 }} /> ))} </div> );}

Display all agents under a single avatar

If you’d prefer all AI agents in the room to be displayed as single avatar, instead of showing a single avatar for each, modify your stack to account for this. This snippet assumes each agent’s user ID begins with agent-.

import { useOthers } from "@liveblocks/react/suspense";
function AvatarStack() { const others = useOthers(); const agent = others.find(({ userId }) => userId.startsWith("agent-")); const humans = others.filter(({ userId }) => !userId.startsWith("agent-"));
return ( <div style={{ display: "flex", marginLeft: 4 }}> {humans.map(({ connectionId, presence, info }) => ( <img key={connectionId} src={info.avatar} alt={info.name} style={{ width: 20, height: 20, borderRadius: "50%", marginLeft: -4 }} /> ))} {agent && ( <img src={agent.info.avatar} alt={agent.info.name} style={{ width: 20, height: 20, borderRadius: "50%", marginLeft: -4 }} /> )} </div> );}