Files
upstream-tldraw/apps/docs/content/sdk-features/user-following.mdx
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

150 lines
7.3 KiB
Plaintext

---
title: User following
created_at: 12/20/2025
updated_at: 12/20/2025
keywords:
- follow
- collaboration
- viewport
- presence
- multiplayer
status: published
date: 12/20/2025
readability: 9
voice: 8
completeness: 9
accuracy: 9
notes: Could use more question-style transitions per voice guide.
---
User following lets you track a collaborator's viewport. When you follow someone, your [camera](/sdk-features/camera) moves to match their view, including page changes and zoom level. The editor handles follow chains (A follows B who follows C) and interpolates your viewport toward the target on each frame.
Following works with the [collaboration](/docs/collaboration) presence system, so it needs a collaborative store. User ids are branded [TLUserId](?) values: take them from presence records, or create one with [createUserId](?). The default People menu already exposes this: clicking a collaborator zooms to them, and the follow button beside their name starts or stops following. The API below is what it calls.
```ts
import { createUserId } from 'tldraw'
const userId = createUserId('user-abc-123')
editor.startFollowingUser(userId)
editor.stopFollowingUser()
editor.zoomToUser(userId)
```
Following stops when you interact with the canvas or when the followed user disconnects.
## How it works
### Follow mechanism
When you call [Editor#startFollowingUser](?), the editor stores the target user ID in `followingUserId` on the instance state and sets up two reactive processes: one watches for page changes and switches pages when the followed user navigates, and another runs on every frame to interpolate your camera toward their viewport.
The frame handler calculates the difference between your current viewport and the target, then interpolates using your animation speed preference. The camera moves quickly at first and slows as it approaches. Once the viewport difference drops below the `followChaseViewportSnap` threshold (default 2 pixels), the editor locks onto the target and mirrors their viewport exactly instead of interpolating. If the animation speed preference is 0, it locks on immediately.
### Viewport calculation
The editor reads the followed user's camera state and screen bounds from their presence record, constructs their viewport in page space, then resizes your viewport to contain theirs while maintaining your screen's aspect ratio. You can see everything the followed user sees, regardless of screen size.
The interpolation uses `lerp` on viewport bounds rather than camera values directly, which produces more natural movement when zoom levels differ. The interpolation factor clamps between 0.1 and 0.8 based on the animation speed preference, so following is never sluggish or aggressive.
### Page synchronization
Page changes happen separately from camera movement. A reactive effect watches the followed user's `currentPageId` and immediately switches [pages](/sdk-features/pages) when it changes. The page switch uses a direct store update rather than `setCurrentPage()` to avoid triggering the automatic follow-stopping behavior that occurs on manual page changes.
When switching pages to follow a user, the editor pauses camera interpolation until the page switch completes. This avoids jarring camera movements during page transitions.
## API methods
### startFollowingUser
Start following a user's viewport. Pass a user ID to set up frame handlers that track their camera and page.
```ts
editor.startFollowingUser(userId)
```
The editor automatically stops following any previously followed user before starting the new follow. If it can't find the target user's presence, the method returns early without starting follow mode.
### stopFollowingUser
[Editor#stopFollowingUser](?) stops following and returns control to the local user. This commits the current camera position to the store, clears `followingUserId` from instance state, and emits the editor's `stop-following` event.
```ts
editor.stopFollowingUser()
```
Following stops automatically when you pan or zoom the canvas. Manual page changes also stop following unless the follow system itself initiated the page change.
### zoomToUser
[Editor#zoomToUser](?) animates the camera to a user's cursor position without entering follow mode. Use this for one-time navigation to a collaborator's location.
```ts
editor.zoomToUser(userId)
editor.zoomToUser(userId, { animation: { duration: 200 } })
```
If you're already following someone, `zoomToUser()` stops following first. The method switches pages if necessary, centers the camera on the user's cursor, and briefly highlights their cursor. The highlight clears after `collaboratorIdleTimeoutMs` (default 3 seconds). If the user is on a different page, the camera jumps without animating.
## Follow chains
When user A follows user B who is following user C, user A follows user C directly rather than B's view of C. The editor traverses the follow chain until it finds a user who isn't following anyone else.
The traversal maintains a visited set to prevent infinite loops if users create a circular follow chain. If a cycle is detected, it returns the last valid presence before the cycle.
```ts
// A follows B follows C
// A's camera tracks C's viewport, not B's viewport
editor.startFollowingUser(userB)
// Internally resolves to C's presence if B is following C
```
The follow chain resolution runs on every frame, so changes in the chain (like B stopping following C) immediately affect A's target viewport without requiring A to restart following.
## UI integration
### FollowingIndicator
The [DefaultFollowingIndicator](?) component displays a colored border around the canvas when you're following someone. It reads `followingUserId` from instance state and fetches the corresponding presence record to get the user's color.
```tsx
import { TLUserId, useEditor, usePresence, useValue } from 'tldraw'
export function DefaultFollowingIndicator() {
const editor = useEditor()
const followingUserId = useValue('follow', () => editor.getInstanceState().followingUserId, [
editor,
])
if (!followingUserId) return null
return <FollowingIndicatorInner userId={followingUserId} />
}
function FollowingIndicatorInner({ userId }: { userId: TLUserId }) {
const presence = usePresence(userId)
if (!presence) return null
return <div className="tlui-following-indicator" style={{ borderColor: presence.color }} />
}
```
You can override it through the `FollowingIndicator` slot of [TLUiComponents](?) to change its appearance or add the followed user's name. See [UI components](/sdk-features/ui-components).
### Follow state detection
Check `editor.getInstanceState().followingUserId` to detect when a user is following someone. Use this to show UI elements like a "Stop following" button or status indicators.
```ts
const followingUserId = editor.getInstanceState().followingUserId
if (followingUserId) {
// Show stop following button
}
```
The presence records from [Editor#getCollaborators](?) include each user's `followingUserId`, so you can visualize the entire follow graph and show which users are following whom.
## Related articles
- [Camera](/sdk-features/camera) - Viewport and camera control
- [Collaboration](/docs/collaboration) - Set up multiplayer presence and sync
- [Pages](/sdk-features/pages) - Page management
- [UI components](/sdk-features/ui-components) - Customize UI elements like the following indicator