snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
189
apps/docs/content/sdk-features/groups.mdx
Normal file
189
apps/docs/content/sdk-features/groups.mdx
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: Groups
|
||||
created_at: 12/25/2025
|
||||
updated_at: 12/29/2025
|
||||
keywords:
|
||||
- groups
|
||||
- grouping
|
||||
- focused group
|
||||
- hierarchy
|
||||
- nesting
|
||||
status: published
|
||||
date: 12/25/2025
|
||||
readability: 9
|
||||
voice: 9
|
||||
completeness: 9
|
||||
accuracy: 9
|
||||
notes: Add API link formatting using [Editor#methodName](?) syntax. Fix blockquote formatting for examples link.
|
||||
---
|
||||
|
||||
Groups are logical containers that combine multiple shapes into a single selectable unit. Unlike frames, groups draw nothing of their own apart from a dashed outline while the group is focused. Their geometry is the union of their children's geometries, their bounds update automatically as children change, and they clean themselves up when their contents are deleted. Groups can be nested inside other groups.
|
||||
|
||||
## Creating groups
|
||||
|
||||
Use [Editor#groupShapes](?) to combine shapes into a group. Pass an array of shape IDs or shape objects, and the method creates a new group containing those shapes:
|
||||
|
||||
```typescript
|
||||
import { createShapeId } from 'tldraw'
|
||||
|
||||
// Group selected shapes
|
||||
editor.groupShapes(editor.getSelectedShapeIds())
|
||||
|
||||
// Group specific shapes
|
||||
editor.groupShapes([shapeId1, shapeId2, shapeId3])
|
||||
|
||||
// Group with options
|
||||
editor.groupShapes([shapeId1, shapeId2], {
|
||||
groupId: createShapeId('my-group'), // Custom ID for the group
|
||||
select: false, // Don't select the group after creation
|
||||
})
|
||||
```
|
||||
|
||||
Grouping requires at least two shapes. The method does nothing if you pass fewer, and it skips [locked shapes](/sdk-features/locked-shapes). Users can also group shapes with Ctrl+G (Cmd+G on Mac); when the only selected shape is a group, the same shortcut ungroups it.
|
||||
|
||||
The new group is positioned at the top-left of the combined bounds of all grouped shapes. It takes the z-index of the topmost grouped shape, so it sits where that shape was in its parent's layer stack.
|
||||
|
||||
## Ungrouping
|
||||
|
||||
Use [Editor#ungroupShapes](?) to dissolve groups and release their children:
|
||||
|
||||
```typescript
|
||||
// Ungroup selected groups
|
||||
editor.ungroupShapes(editor.getSelectedShapeIds())
|
||||
|
||||
// Ungroup specific groups
|
||||
editor.ungroupShapes([groupId])
|
||||
|
||||
// Ungroup without selecting the released children
|
||||
editor.ungroupShapes([groupId], { select: false })
|
||||
```
|
||||
|
||||
Ungrouping moves children to the group's parent and preserves their page positions and rotations. Layer order is maintained: children appear where the group was in the z-stack. If you pass a mix of groups and non-groups, only the groups are ungrouped and the non-groups stay selected. Users can ungroup with Ctrl+Shift+G (Cmd+Shift+G on Mac).
|
||||
|
||||
Ungrouping is not recursive. If a group contains other groups, those inner groups remain intact. Ungroup them separately if needed.
|
||||
|
||||
## Focused groups
|
||||
|
||||
The editor tracks a focused group that defines the current editing scope. When you're focused inside a group, you can select and manipulate the shapes within it. Without focus, clicking a shape inside a group selects the group itself, not the individual shape.
|
||||
|
||||
Use [Editor#getFocusedGroup](?), [Editor#getFocusedGroupId](?), [Editor#setFocusedGroup](?), and [Editor#popFocusedGroupId](?):
|
||||
|
||||
```typescript
|
||||
// Get the current focused group
|
||||
const focusedGroup = editor.getFocusedGroup()
|
||||
|
||||
// Get the focused group ID (returns page ID if no group is focused)
|
||||
const focusedId = editor.getFocusedGroupId()
|
||||
|
||||
// Focus a specific group
|
||||
editor.setFocusedGroup(groupId)
|
||||
|
||||
// Exit the current focused group and select it
|
||||
editor.popFocusedGroupId()
|
||||
```
|
||||
|
||||
The editor manages focus automatically based on selection:
|
||||
|
||||
- Selecting a shape inside a group focuses that group
|
||||
- Selecting shapes across multiple groups focuses their common ancestor
|
||||
- Pressing Escape pops focus one level up (or back to the page)
|
||||
- Pressing Enter with only groups selected selects their children
|
||||
- Clearing selection keeps the current focus
|
||||
|
||||
### Layered selection
|
||||
|
||||
Clicking shapes inside groups follows a layered pattern:
|
||||
|
||||
1. First click selects the outermost group
|
||||
2. Second click focuses the group and selects the parent of the target shape (or the shape if directly inside)
|
||||
3. Further clicks drill down through nested groups
|
||||
|
||||
This lets you work at different levels of the hierarchy without keyboard modifiers.
|
||||
|
||||
## Nested groups
|
||||
|
||||
Groups can contain other groups, creating hierarchies:
|
||||
|
||||
```typescript
|
||||
// Create a nested structure
|
||||
editor.select(boxA, boxB)
|
||||
editor.groupShapes(editor.getSelectedShapeIds())
|
||||
const innerGroupId = editor.getOnlySelectedShapeId()
|
||||
|
||||
editor.select(innerGroupId, boxC, boxD)
|
||||
editor.groupShapes(editor.getSelectedShapeIds())
|
||||
const outerGroupId = editor.getOnlySelectedShapeId()
|
||||
|
||||
// Result:
|
||||
// outerGroup
|
||||
// ├── innerGroup
|
||||
// │ ├── boxA
|
||||
// │ └── boxB
|
||||
// ├── boxC
|
||||
// └── boxD
|
||||
```
|
||||
|
||||
When grouping shapes that already have different parents, the editor finds their common ancestor and creates the group there. This prevents orphaning shapes from their natural hierarchy.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Groups maintain themselves through the [ShapeUtil#onChildrenChange](?) lifecycle hook. If you delete all children of a group, the group is removed. If a group ends up with only one child, it dissolves and reparents that child to the group's parent.
|
||||
|
||||
This cleanup happens immediately when children change, so you never end up with degenerate groups.
|
||||
|
||||
```typescript
|
||||
// Start with a group containing boxA and boxB
|
||||
editor.deleteShapes([boxA])
|
||||
// Group now has only boxB, so it auto-ungroups
|
||||
// boxB is now a direct child of the page (or the group's former parent)
|
||||
```
|
||||
|
||||
## Bounds and transforms
|
||||
|
||||
Group bounds are computed from their children's geometries. The [Group2d](?) geometry class aggregates all child geometries for hit testing and bounds calculation:
|
||||
|
||||
```typescript
|
||||
// Get a group's bounds
|
||||
const bounds = editor.getShapePageBounds(groupId)
|
||||
|
||||
// Bounds update automatically when children move
|
||||
editor.updateShape({ id: childId, type: 'geo', x: newX, y: newY })
|
||||
const updatedBounds = editor.getShapePageBounds(groupId)
|
||||
```
|
||||
|
||||
When you transform a group (move, rotate, resize), all children transform with it. The group's position and rotation are applied to children through the standard parent-child transform composition.
|
||||
|
||||
Position preservation works both ways. When you group shapes, their page positions are preserved—only their `parentId` and local coordinates change. When you ungroup, shapes return to their original page positions even if the group was rotated.
|
||||
|
||||
## Creating shapes inside groups
|
||||
|
||||
When you create new shapes while focused inside a group, those shapes become children of the focused group unless they're positioned over a container such as a frame, which takes precedence:
|
||||
|
||||
```typescript
|
||||
// Focus a group by selecting a shape inside it
|
||||
editor.select(shapeInsideGroup)
|
||||
// Now getFocusedGroupId() returns the group's ID
|
||||
|
||||
// Create a new shape - it becomes a child of the focused group
|
||||
editor.createShape({
|
||||
type: 'geo',
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: { w: 50, h: 50 },
|
||||
})
|
||||
// The new shape's parentId is the focused group
|
||||
```
|
||||
|
||||
If no group is focused, new shapes are created on the current page.
|
||||
|
||||
## Limitations
|
||||
|
||||
Arrows can't bind to groups. [ShapeUtil#canBind](?) returns `false` for group shapes, so arrows must bind to individual shapes within the group.
|
||||
|
||||
Groups have no visual properties. You can't style a group itself, only its children.
|
||||
|
||||
Both grouping and ungrouping require the select tool to be active, and do nothing in readonly mode. If you're in the middle of another interaction, the editor cancels it before running the operation.
|
||||
|
||||
## Related examples
|
||||
|
||||
- [Layer panel](/examples/ui/layer-panel) - Build a hierarchical layer panel that shows shape and group structure with visibility controls.
|
||||
Reference in New Issue
Block a user