apps/docs/content/sdk-features/parenting.mdx
Every shape in tldraw has a parent: either the page it lives on or another shape that contains it. Groups and frames use this hierarchy to hold other shapes. The editor uses it for transforms, selection, and rendering order.
Every shape record has a parentId property that points to its parent. For shapes directly on the canvas, this is a page ID. For shapes inside groups or frames, it's the containing shape's ID:
// A shape on the page
const shape = editor.getShape(myShapeId)
console.log(shape.parentId) // "page:somePage"
// A shape inside a group
const groupedShape = editor.getShape(childShapeId)
console.log(groupedShape.parentId) // "shape:someGroup"
You can check what type of parent a shape has using isPageId and isShapeId, both exported from tldraw:
import { isPageId, isShapeId } from 'tldraw'
if (isPageId(shape.parentId)) {
// Shape is directly on a page
}
if (isShapeId(shape.parentId)) {
// Shape is inside another shape (group, frame, etc.)
}
Use Editor#getShapeParent to get the parent shape. It returns undefined if the shape is directly on a page:
const parent = editor.getShapeParent(myShape)
if (parent) {
console.log('Parent shape:', parent.type)
} else {
console.log('Shape is on the page')
}
The ancestor chain is the path from a shape up to the page. Use Editor#getShapeAncestors to get all ancestors in order from the root to the immediate parent:
// For a deeply nested shape:
// page > frameA > groupB > myShape
const ancestors = editor.getShapeAncestors(myShapeId)
// Returns: [frameA, groupB]
The array is ordered from root ancestor to immediate parent. The page itself is never included—ancestors only contain shapes.
Use Editor#findShapeAncestor to find the first ancestor matching a condition:
// Find the containing frame
const frame = editor.findShapeAncestor(myShape, (ancestor) => ancestor.type === 'frame')
// Find the first locked ancestor
const lockedAncestor = editor.findShapeAncestor(myShape, (ancestor) => ancestor.isLocked)
Use Editor#hasAncestor to check if a shape is inside a specific container:
if (editor.hasAncestor(myShape, frameId)) {
// myShape is somewhere inside this frame
}
When working with multiple shapes, use Editor#findCommonAncestor to find their nearest shared parent:
const shapeIds = [shapeA, shapeB, shapeC]
const commonAncestorId = editor.findCommonAncestor(shapeIds)
if (commonAncestorId) {
// All shapes share this ancestor
} else {
// Shapes are on the page with no common parent shape
}
You can also filter by a predicate:
// Find the common frame ancestor
const commonFrame = editor.findCommonAncestor(shapeIds, (shape) => shape.type === 'frame')
Use Editor#getSortedChildIdsForParent to get a shape's children in z-index order:
const childIds = editor.getSortedChildIdsForParent(groupId)
// Returns child IDs sorted from back to front
This works for pages too:
const topLevelShapes = editor.getSortedChildIdsForParent(editor.getCurrentPageId())
For recursive traversal, use Editor#visitDescendants:
editor.visitDescendants(frameId, (childId) => {
const child = editor.getShape(childId)
console.log('Found:', child.type)
// Return false to skip this shape's children
})
To collect all descendants including the shape itself, use Editor#getShapeAndDescendantIds:
const allIds = editor.getShapeAndDescendantIds([frameId])
// Returns a Set containing frameId and all nested shape IDs
Use Editor#reparentShapes to move shapes into a new parent. This preserves the shapes' page positions—only their local coordinates change to match the new parent's coordinate space:
// Move shapes into a frame
editor.reparentShapes([shapeA, shapeB], frameId)
// Move shapes to the page root
editor.reparentShapes([shapeA, shapeB], editor.getCurrentPageId())
The method handles coordinate transformation automatically. If the parent is rotated, children's positions and rotations are adjusted so they appear in the same place on the page.
You can pass an IndexKey (a fractional index string, not a numeric position) as the third argument to control z-ordering. The reparented shapes are inserted at that key among the new parent's children:
// Put the child where the group sat in its parent's stack
editor.reparentShapes([newChild], group.parentId, group.index)
When you omit parentId from Editor#createShapes, the editor picks one for you: the focused group, or a container such as a frame under the shape's x and y. Deleting a shape with Editor#deleteShapes also deletes its descendants.
Parent-child relationships affect coordinate systems. A child shape's x and y are relative to its parent, not the page.
Use Editor#getPointInShapeSpace and Editor#getShapePageTransform to convert between coordinate systems:
// Convert a page point to a shape's local space
const localPoint = editor.getPointInShapeSpace(parentShape, pagePoint)
// Get a shape's position in page coordinates
const pageTransform = editor.getShapePageTransform(childShape)
const pagePoint = pageTransform.point()
Editor#getShapeParentTransform and Editor#getShapeLocalTransform give you the other two pieces of the composition.
When you move a parent, all children move with it. Their local coordinates stay the same, but their page coordinates change.
For more about coordinate systems and transforms, see Coordinates.
Use Editor#isShapeInPage to check if a shape is on a specific page (even if nested):
if (editor.isShapeInPage(myShape, pageId)) {
// Shape is on this page (directly or nested)
}
To get the page a shape belongs to, use Editor#getAncestorPageId:
const pageId = editor.getAncestorPageId(myShape)
A shape is effectively locked if any of its ancestors are locked. Use Editor#isShapeOrAncestorLocked to check:
if (editor.isShapeOrAncestorLocked(myShape)) {
// Shape can't be interacted with
}
The editor uses this for pointer interactions and Editor#updateShapes. Bulk operations such as Editor#deleteShapes only check each shape's own isLocked. See Locked shapes.
Visibility also inherits through the hierarchy: a shape is hidden if the getShapeVisibility prop returns 'hidden' for it or for any ancestor, unless the shape returns 'visible'. Check with Editor#isShapeHidden. See Visibility.
The editor tracks a "focused group" that determines which level of the hierarchy you're working in. When you're focused inside a group, new shapes are created as children of that group. See Groups for details on focused groups.