apps/docs/content/sdk-features/camera.mdx
The camera controls which part of the infinite canvas is visible. It holds the viewport's position and zoom, converts between screen space and page space, and responds to user input like wheel, trackpad, keyboard, and touch. You can constrain it to a bounded area, move it programmatically with animation, and follow other users' viewports in collaborative sessions.
The camera represents the viewport's position and zoom using three values: x and y for position in page space, and z for zoom level. A zoom of 1 means 100%, 0.5 is 50%, and 2 is 200%. The camera's x and y are the page-space offset of the viewport: the page point at the viewport's top-left corner is (-x, -y).
The camera converts between screen space (browser pixels) and page space (the canvas). Use Editor#screenToPage to turn a mouse position into a canvas point and Editor#pageToScreen to go the other way. See Coordinates for the full set of conversions.
const pagePoint = editor.screenToPage({ x: event.clientX, y: event.clientY })
const screenPoint = editor.pageToScreen({ x: shape.x, y: shape.y })
Read the camera with Editor#getCamera or Editor#getZoomLevel, and read the visible page area with Editor#getViewportPageBounds. All of these are reactive, so you can use them in track components or useValue.
Configure the camera with TLCameraOptions. Pass the initial options through the options.camera prop, or change them at runtime with Editor#setCameraOptions, which re-applies the constraints to the current camera immediately:
<Tldraw options={{ camera: { wheelBehavior: 'zoom' } }} />
editor.setCameraOptions({
isLocked: false,
wheelBehavior: 'pan',
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
})
Set isLocked to freeze the camera for fixed-viewport apps. Camera methods then do nothing unless you pass force: true.
wheelBehavior decides what the mouse wheel or trackpad scroll does: 'pan', 'zoom', or 'none'. If the user has set an input mode in their preferences, that preference wins: 'trackpad' pans and 'mouse' zooms.
panSpeed and zoomSpeed are multipliers on input sensitivity. Values below 1 slow movement down, values above 1 speed it up.
zoomSteps lists the discrete zoom levels used by zoom in and zoom out. The first value is the minimum zoom and the last is the maximum; the camera clamps to this range even without constraints.
Camera constraints limit where users can navigate. Use them for presentations, guided experiences, or applications with fixed content areas:
editor.setCameraOptions({
constraints: {
bounds: { x: 0, y: 0, w: 1920, h: 1080 },
padding: { x: 50, y: 50 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min',
baseZoom: 'default',
behavior: 'inside',
},
})
The bounds define the constrained area in page space. The camera restricts panning outside this rectangle based on the behavior setting.
The padding adds a screen-space margin inside the viewport so content doesn't touch the edges.
The origin positions the bounds within the viewport when an axis uses 'fixed' behavior, when 'contain' is zoomed out below the fit zoom, and when the camera resets. { x: 0.5, y: 0.5 } centers the bounds; { x: 0, y: 0 } aligns them top-left.
initialZoom is the zoom the camera starts at and returns to on reset. baseZoom is the zoom that zoomSteps are multiplied by, so a step of 1 means "the base zoom" rather than 100%. Both accept the same values:
| Value | Description |
|---|---|
'default' | 100% zoom |
'fit-x' | The bounds' width fills the viewport width |
'fit-y' | The bounds' height fills the viewport height |
'fit-min' | The smaller axis fills the viewport; the larger axis may extend past it |
'fit-max' | The larger axis fills the viewport, so the full bounds stay visible |
'fit-x-100' | fit-x or 100%, whichever is smaller |
'fit-y-100' | fit-y or 100%, whichever is smaller |
'fit-min-100' | fit-min or 100%, whichever is smaller |
'fit-max-100' | fit-max or 100%, whichever is smaller |
The behavior option controls how the bounds constrain camera movement:
| Value | Description |
|---|---|
'free' | The bounds are ignored |
'fixed' | The bounds are pinned at the origin; the user can't pan |
'inside' | The bounds stay completely within the viewport |
'outside' | The bounds stay touching the viewport |
'contain' | 'fixed' when zoomed out below the fit zoom, 'inside' when zoomed in past it |
Set behavior per axis for asymmetric constraints:
behavior: {
x: 'free', // Horizontal panning unrestricted
y: 'inside', // Vertical panning keeps bounds visible
}
The camera-move methods below (Editor#setCamera, Editor#centerOnPoint, Editor#zoomIn, Editor#zoomOut, Editor#zoomToFit, Editor#zoomToSelection, Editor#zoomToBounds, and Editor#resetZoom) accept optional TLCameraMoveOptions:
animation - animate the move with duration and easingimmediate - move the camera immediately rather than on the next tickforce - move the camera even when isLocked is truereset - reset the camera to the constraints' initial zoom and originMove the camera to a specific position and zoom:
editor.setCamera({ x: -500, y: -300, z: 1.5 })
Center the viewport on a point:
editor.centerOnPoint({ x: 1000, y: 500 })
Zoom in or out. Both methods accept an optional screen point to zoom toward:
editor.zoomIn()
editor.zoomOut()
editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
Focus the camera on shapes or bounds:
// Fit all shapes on the current page
editor.zoomToFit()
// Fit the current selection
editor.zoomToSelection()
// Reset zoom to 100%. With constraints, toggles between the initial zoom and 100%
editor.resetZoom()
// Fit specific bounds with padding
const bounds = { x: 0, y: 0, w: 1000, h: 800 }
editor.zoomToBounds(bounds, { inset: 100 })
zoomToBounds accepts inset to add screen-space padding around the bounds and targetZoom to cap the zoom level.
Add smooth transitions with the animation option. The EASINGS object provides common easing functions:
import { EASINGS } from 'tldraw'
editor.setCamera(
{ x: 0, y: 0, z: 1 },
{
animation: {
duration: 500,
easing: EASINGS.easeInOutCubic,
},
}
)
Camera animations stop automatically when the user pans or zooms: user input takes precedence over programmatic movement. You can also stop them at any time with Editor#stopCameraAnimation. See Animation for more on animation options and user preferences.
Use Editor#slideCamera for kinetic scrolling, for example to keep the camera moving after a gesture ends:
editor.slideCamera({
speed: 1,
direction: { x: 1, y: 0 },
friction: 0.1,
speedThreshold: 0.01,
})
speed is clamped to a maximum of 1 and direction sets the initial velocity. A z component on direction slides the zoom as well. friction controls how fast the camera decelerates (higher stops sooner) and defaults to editor.options.cameraSlideFriction. The slide ends once the speed drops below speedThreshold.
The default tldraw UI has a quick zoom mode. Press z to select the zoom tool, then hold Shift. The camera zooms out to show your current viewport plus everything on the page, and a brush marks where you'll land. Move the cursor to place the brush and release Shift to zoom there. Press Escape to cancel and return to the original view.
Call Editor#startFollowingUser to track another user's viewport, or Editor#zoomToUser to jump to their cursor once. While following, the camera fits the other user's viewport inside yours: if the aspect ratios differ, the zoom adjusts so their whole viewport stays visible.
See User following for details.
zoomToBounds.