apps/docs/content/sdk-features/selection.mdx
The editor tracks which shapes are selected. You can change the selection, read the selected shapes, and get their combined bounds and rotation.
The editor also enforces a few rules on its own: you can't select both a group and its children at the same time, and selecting shapes inside a group focuses that group.
The editor tracks selection through the selectedShapeIds array in the current page's instance state. This array holds the IDs of all currently selected shapes. Everything else about the selection (bounds, rotation, the selected shape records) is derived from it.
// Get currently selected shape IDs
const selectedIds = editor.getSelectedShapeIds()
// Get the actual shape objects
const selectedShapes = editor.getSelectedShapes()
Editor#getSelectedShapes resolves the IDs to shape records and drops any IDs that no longer exist in the store.
Use Editor#select, Editor#setSelectedShapes, Editor#deselect, and Editor#selectNone to change the selection:
// Select specific shapes (replaces current selection)
editor.select(shapeId1, shapeId2)
editor.setSelectedShapes([shapeId1, shapeId2])
// Deselect specific shapes (removes from current selection)
editor.deselect(shapeId1)
// Clear all selection
editor.selectNone()
Both select() and setSelectedShapes() replace the current selection entirely. Use deselect() to remove specific shapes while keeping others selected. Selection changes are recorded in history without clearing the redo stack.
Editor#selectAll selects all unlocked shapes, scoped by the current selection:
editor.selectAll()
Use Editor#selectAdjacentShape to select the next or previous shape in reading order, or to move by cardinal direction:
editor.selectAdjacentShape('next')
editor.selectAdjacentShape('prev')
editor.selectAdjacentShape('left')
editor.selectAdjacentShape('right')
editor.selectAdjacentShape('up')
editor.selectAdjacentShape('down')
Cardinal directions score candidate shapes by distance and by how far they sit off the axis of travel, then pick the lowest score. If the selection is inside a group or frame, only siblings in that container are considered. Shapes whose util returns false from ShapeUtil#canTabTo are skipped. In the default UI, Tab and Shift+Tab move to the next and previous shape, and Cmd/Ctrl+Arrow moves by direction.
Use Editor#selectParentShape and Editor#selectFirstChildShape to move up and down the shape hierarchy:
// Select the parent of the currently selected shape
editor.selectParentShape()
// Select the first child of the currently selected shape
editor.selectFirstChildShape()
selectParentShape() only acts when exactly one shape is selected. selectFirstChildShape() picks the first child (in reading order) of the first selected shape. Both zoom to the new selection if it's offscreen. In the default UI these are bound to Cmd/Ctrl+Shift+Up and Cmd/Ctrl+Shift+Down.
When you need to work with exactly one selected shape, use Editor#getOnlySelectedShapeId and Editor#getOnlySelectedShape:
// Get the ID if exactly one shape is selected, null otherwise
const id = editor.getOnlySelectedShapeId()
// Get the shape if exactly one shape is selected, null otherwise
const shape = editor.getOnlySelectedShape()
Both methods return null if zero shapes or multiple shapes are selected.
To find which selected shape is at a specific point (useful for hit testing during interactions), use Editor#getSelectedShapeAtPoint:
const shape = editor.getSelectedShapeAtPoint({ x: 100, y: 200 })
This returns the top-most selected shape at the given point, ignoring groups. It returns undefined if no selected shape is at that point.
The editor computes bounds for the current selection in two ways: axis-aligned and rotated.
Editor#getSelectionPageBounds returns the axis-aligned bounding box that contains all selected shapes:
const bounds = editor.getSelectionPageBounds()
if (bounds) {
console.log(bounds.x, bounds.y, bounds.width, bounds.height)
}
If the selection includes rotated shapes, these bounds represent the smallest axis-aligned box that contains the rotated shapes. The method returns null if nothing is selected.
Editor#getSelectionRotatedPageBounds returns bounds that respect the shared rotation of the selection:
const rotatedBounds = editor.getSelectionRotatedPageBounds()
The selection box UI uses this for display. If all selected shapes share the same rotation, the bounds rotate with them. If shapes have different rotations, this falls back to axis-aligned bounds. It returns undefined if nothing is selected.
You can access the shared rotation angle via Editor#getSelectionRotation, which returns 0 if shapes have different rotations.
Both bound types have screen-space equivalents, Editor#getSelectionScreenBounds and Editor#getSelectionRotatedScreenBounds, that account for the camera's zoom and pan:
const screenBounds = editor.getSelectionScreenBounds()
const rotatedScreenBounds = editor.getSelectionRotatedScreenBounds()
The editor automatically enforces selection consistency through store side effects.
When the selection changes, the editor filters out any shape whose ancestor is also selected:
// If you try to select a shape and its parent, only the parent remains selected
editor.select(groupId, childOfGroupId)
// Result: only groupId is selected
This prevents ambiguous situations where both a container and its contents are selected. The filtering happens in the instance_page_state after-change side effect.
When you select shapes that are children of a group, the editor automatically updates the focused group. The focused group is the group that defines the current editing scope: while a group is focused, clicks select shapes inside it rather than the group itself. See Groups for details.
// Selecting shapes inside a group focuses that group
editor.select(shapeInsideGroup)
// The group becomes the focused group
If all selected shapes share a common group ancestor, that group becomes focused. If you select shapes without a common group ancestor, the editor clears the focused group. Clearing the selection leaves the focused group in place.
The editor excludes locked shapes from bulk selection operations:
// selectAll only selects unlocked shapes
editor.selectAll()
// Operations like delete and duplicate also respect locks
editor.deleteShapes(shapeIds) // Only deletes unlocked shapes
Locks don't restrict individual shape selection through select(). You can still select locked shapes explicitly when needed. By default, users can't select locked shapes by clicking or brushing; the selectLockedShapes option in TldrawOptions allows that while keeping the shapes protected from edits. See Locked shapes.
To determine if a shape's ancestor is selected, use Editor#isAncestorSelected:
const hasSelectedAncestor = editor.isAncestorSelected(shape)
This walks up the shape's parent chain and returns true if any ancestor is in the current selection.