---
meta:
  title: "Presence"
  parentTitle: "Sync"
  description:
    "Share temporary user and agent state such as cursors, selections, and
    active tools."
---

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`](/docs/api-reference/liveblocks-react-ui#Cursors) and
[`AvatarStack`](/docs/api-reference/liveblocks-react-ui#AvatarStack), which
correspondingly display realtime cursors and avatars.

```tsx
import { AvatarStack, Cursors } from "@liveblocks/react-ui";

function App() {
  return (
    <div>
      // +++
      <Cursors />
      <AvatarStack />
      // +++
    </div>
  );
}
```

Follow the [Presence quickstart guide](/docs/get-started/nextjs-presence) 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.

```ts 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`](/docs/api-reference/liveblocks-react#RoomProvider). In this
snippet, `null` represents a cursor that is offscreen.

```tsx
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.

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

    // +++
    UserInfo: {
      name: string;
      color: string;
      avatar: string;
    };
    // +++
  }
}
```

Next, when [authenticating](/docs/api-reference/authentication) 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.

```ts
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});

export async function POST(request: Request) {
  const user = __getServerSession__();

  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`](/docs/api-reference/liveblocks-react#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.

```tsx
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`](/docs/api-reference/liveblocks-react#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](#Set-up-user-info) from before to show their name and color.

```tsx
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](#Set-up-user-info), you can render a live avatar stack
wuth [`useOthers`](/docs/api-reference/liveblocks-react#useOthers) and your
`info.avatar` property.

```tsx
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`](/docs/api-reference/liveblocks-react#useUpdateMyPresence)
to set `typing` to `true` on input, then clear it with a timeout after the user
stops typing.

```tsx file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // +++
    Presence: {
      typing: boolean;
    };
    // +++
  }
}
```

```tsx
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`](/docs/api-reference/liveblocks-react#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.

```tsx file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // +++
    Presence: {
      selection: string | null;
    };
    // +++
  }
}
```

```tsx
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`](/docs/api-reference/liveblocks-node#post-rooms-roomId-presence)
method is used for this, and after it’s called,
[`useOthers`](/docs/api-reference/liveblocks-react#useOthers) will return the
agent in the list of connected users. `data` and `userInfo` correspond to
`Presence` and `UserInfo` in your types.

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

```ts
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](/docs/products/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.

```ts file="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.

```ts
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.

```tsx
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-`.

```tsx
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>
  );
}
```

---

For an overview of all available documentation, see [/llms.txt](/llms.txt).
