Files
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

266 lines
10 KiB
Plaintext

---
title: Drag and drop
created_at: 2/4/2026
updated_at: 2/4/2026
keywords:
- drag
- drop
- reparent
- container
- frame
status: published
date: 2/4/2026
---
The drag and drop system lets shapes respond when other shapes are dragged over them. Frames use this to reparent shapes when you drag them inside. You can use the same callbacks in your [ShapeUtil](?) to build custom container shapes or any shape that should react to shapes being dragged over it.
If you want frame-like behavior (accept children, reparent them on drag in and out, clip them), extend [BaseFrameLikeShapeUtil](?), which implements all of the callbacks below. Implement them yourself when you need something different.
## Drag callbacks
While the select tool translates shapes, it tracks which shape (if any) is under the cursor using [Editor#getDraggingOverShape](?). Your shape util can implement these callbacks to respond:
| Callback | When it fires |
| ------------------------------- | --------------------------------------------------------------------------------- |
| [ShapeUtil#onDragShapesIn](?) | Shapes are first dragged over this shape |
| [ShapeUtil#onDragShapesOver](?) | Shapes continue being dragged over this shape (on an interval, when cursor moves) |
| [ShapeUtil#onDragShapesOut](?) | Shapes are dragged away from this shape |
| [ShapeUtil#onDropShapesOver](?) | Shapes are dropped onto this shape |
The callbacks receive the target shape (the one being dragged over), an array of the shapes being dragged, and an info object with context about the drag operation. Locked shapes are never dragged, and if every child of a group is being dragged, the group is dragged instead of its children.
### onDragShapesIn
Called once when shapes first enter this shape's bounds. Use it to reparent shapes into a container:
```typescript
override onDragShapesIn(shape: MyContainerShape, draggingShapes: TLShape[]) {
// Only reparent if not already a child
const newShapes = draggingShapes.filter((s) => s.parentId !== shape.id)
if (newShapes.length > 0) {
this.editor.reparentShapes(newShapes, shape.id)
}
}
```
### onDragShapesOver
Called on an interval while shapes are being dragged over this shape, but only when the cursor moves. Use it for visual feedback or custom snapping:
```typescript
override onDragShapesOver(shape: MyGridShape, draggingShapes: TLShape[]) {
// Snap shapes to grid cells while dragging
for (const dragging of draggingShapes) {
const snappedX = Math.round(dragging.x / CELL_SIZE) * CELL_SIZE
const snappedY = Math.round(dragging.y / CELL_SIZE) * CELL_SIZE
if (dragging.x !== snappedX || dragging.y !== snappedY) {
this.editor.updateShape({
id: dragging.id,
type: dragging.type,
x: snappedX,
y: snappedY,
})
}
}
}
```
### onDragShapesOut
Called when shapes are dragged away from this shape, including when the drag moves into another shape. Use it to reparent shapes back to the page:
```typescript
override onDragShapesOut(
shape: MyContainerShape,
draggingShapes: TLShape[],
info: TLDragShapesOutInfo
) {
// Check if we're dragging into another container
if (info.nextDraggingOverShapeId) {
// Let the next container handle it
return
}
// Reparent back to the page
const children = draggingShapes.filter((s) => s.parentId === shape.id)
if (children.length > 0) {
this.editor.reparentShapes(children, this.editor.getCurrentPageId())
}
}
```
### onDropShapesOver
Called when shapes are dropped (mouse up) while over this shape. Use it for finalization logic:
```typescript
override onDropShapesOver(shape: MySlotShape, draggingShapes: TLShape[]) {
// Lock shapes in place after dropping
for (const dragging of draggingShapes) {
this.editor.updateShape({
id: dragging.id,
type: dragging.type,
isLocked: true,
})
}
}
```
## The info object
All drag callbacks receive an info object with context about the drag operation. The exact type varies by callback:
| Callback | Info type |
| ------------------ | ------------------------- |
| `onDragShapesIn` | [TLDragShapesInInfo](?) |
| `onDragShapesOver` | [TLDragShapesOverInfo](?) |
| `onDragShapesOut` | [TLDragShapesOutInfo](?) |
| `onDropShapesOver` | [TLDropShapesOverInfo](?) |
All info types share these common properties:
| Property | Description |
| ---------------------------- | ----------------------------------------------------- |
| `initialDraggingOverShapeId` | The shape that was under the cursor when drag started |
| `initialParentIds` | Map of each shape's parent ID at drag start |
| `initialIndices` | Map of each shape's z-index at drag start |
Some callbacks have additional properties:
| Property | Available in | Description |
| ------------------------- | ----------------- | ---------------------------------------- |
| `prevDraggingOverShapeId` | `onDragShapesIn` | The previous shape that was dragged over |
| `nextDraggingOverShapeId` | `onDragShapesOut` | The next shape being dragged into |
The `initialParentIds` and `initialIndices` maps let you restore shapes to their original positions. Frames use them to preserve z-ordering when shapes are dragged back to their original parent.
## Determining what's under the cursor
[Editor#getDraggingOverShape](?) determines which shape is being dragged over by checking which shape's geometry contains the cursor point. It tests shapes from front to back, skipping locked shapes, hidden shapes, and the shapes being dragged, and returns the first hit.
Only shapes that implement drag callbacks are considered as drop targets. If your shape util doesn't override any drag callbacks, shapes will pass through it when being dragged. While shapes are dragged over a target that accepts them, the editor hints the target with [Editor#setHintingShapes](?).
## Controlling which shapes can be dropped
Use the [ShapeUtil#canReceiveNewChildrenOfType](?) method to control which shape types your container accepts. The editor uses it to decide whether `onDragShapesIn` and `onDropShapesOver` fire for a dragged shape:
```typescript
override canReceiveNewChildrenOfType(shape: MyContainerShape, type: TLShape['type']) {
// Only accept specific shape types
return type === 'my-item' || type === 'geo'
}
```
The default is `false`, so any shape that should accept dropped children must override this method. `onDragShapesOver` is not gated by this method, which lets you provide visual feedback even when the target won't accept the drop.
## Controlling which shapes can be dragged out
Use the [ShapeUtil#canRemoveChildrenOfType](?) method to control which child shape types can be dragged out of your container. The editor uses it to decide whether `onDragShapesOut` fires for a child shape, and to decide whether to automatically [reparent](/sdk-features/parenting) a child that has moved outside its parent's geometry:
```typescript
override canRemoveChildrenOfType(shape: MyContainerShape, type: TLShape['type']) {
// Pin certain children in place; allow others to be removed
return type !== 'pinned-item'
}
```
The default is `true`, so children can be dragged out of any container unless this method is overridden.
## Example: slot container
Here's a complete example of a slot-based container that accepts dropped shapes:
```tsx
import {
HTMLContainer,
Rectangle2d,
ShapeUtil,
TLBaseShape,
TLDragShapesOutInfo,
TLShape,
T,
} from 'tldraw'
type SlotContainerShape = TLBaseShape<'slot-container', { slots: number }>
class SlotContainerShapeUtil extends ShapeUtil<SlotContainerShape> {
static override type = 'slot-container' as const
static override props = { slots: T.number }
getDefaultProps() {
return { slots: 4 }
}
getGeometry(shape: SlotContainerShape) {
return new Rectangle2d({
width: shape.props.slots * 100,
height: 100,
isFilled: true,
})
}
override canReceiveNewChildrenOfType(_shape: SlotContainerShape, type: string) {
return type === 'geo' || type === 'text'
}
override onDragShapesIn(shape: SlotContainerShape, draggingShapes: TLShape[]) {
const newShapes = draggingShapes.filter((s) => s.parentId !== shape.id)
if (newShapes.length > 0) {
this.editor.reparentShapes(newShapes, shape.id)
}
}
override onDragShapesOut(
shape: SlotContainerShape,
draggingShapes: TLShape[],
info: TLDragShapesOutInfo
) {
if (!info.nextDraggingOverShapeId) {
const children = draggingShapes.filter((s) => s.parentId === shape.id)
this.editor.reparentShapes(children, this.editor.getCurrentPageId())
}
}
component(shape: SlotContainerShape) {
return (
<HTMLContainer
style={{
backgroundColor: '#f0f0f0',
border: '2px dashed #ccc',
display: 'grid',
gridTemplateColumns: `repeat(${shape.props.slots}, 100px)`,
}}
>
{Array.from({ length: shape.props.slots }).map((_, i) => (
<div
key={i}
style={{
width: 100,
height: 100,
borderRight: i < shape.props.slots - 1 ? '1px dashed #ccc' : undefined,
}}
/>
))}
</HTMLContainer>
)
}
getIndicatorPath(shape: SlotContainerShape) {
const path = new Path2D()
path.rect(0, 0, shape.props.slots * 100, 100)
return path
}
}
```
## Related examples
- [Drag and drop](/examples/shapes/tools/drag-and-drop) - Custom shapes that can be dragged onto each other.
- [Drag and drop tray](/examples/ui/drag-and-drop-tray) - Drag items from a custom UI into the canvas.
## Related docs
- [External content handling](/sdk-features/external-content) - Handle files, URLs, and other content dragged from outside the browser. The callbacks on this page are for shape-to-shape drag and drop within the canvas.
- [Parenting and ancestors](/sdk-features/parenting) - Learn about parent-child relationships and the [Editor#reparentShapes](?) method.