Locked shapes
Lock shapes to prevent user editing, and use ignoreShapeLock to modify them programmatically.
import { useState } from 'react'
import { createShapeId, Tldraw, TldrawUiButton, TLShapeId, toRichText, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
// [1]
const TEMPLATE_IDS: TLShapeId[] = [
createShapeId('t1'),
createShapeId('t2'),
createShapeId('t3'),
createShapeId('t4'),
]
// [2]
function ControlPanel() {
const editor = useEditor()
// [3] Local state mirrors editor.options.selectLockedShapes for the UI.
// The option is readonly at the type level but the editor stores a copy
// internally that the SelectTool reads live on every interaction — so
// mutating the underlying field flips behaviour immediately, without a
// remount.
const [selectLocked, setSelectLocked] = useState(editor.options.selectLockedShapes)
const toggleSelectLocked = () => {
const next = !selectLocked
;(editor.options as { selectLockedShapes: boolean }).selectLockedShapes = next
setSelectLocked(next)
}
// [4] Update locked shapes using ignoreShapeLock option
// Without ignoreShapeLock: true, these updates would be blocked
const handleScatter = () => {
editor.run(
() => {
editor.updateShapes(
TEMPLATE_IDS.map((id) => ({
id,
type: 'geo',
x: 50 + Math.random() * 300,
y: 50 + Math.random() * 300,
}))
)
},
{ ignoreShapeLock: true }
)
}
const handleReset = () => {
editor.run(
() => {
editor.updateShapes([
{ id: TEMPLATE_IDS[0], type: 'geo', x: 100, y: 100 },
{ id: TEMPLATE_IDS[1], type: 'geo', x: 250, y: 100 },
{ id: TEMPLATE_IDS[2], type: 'geo', x: 100, y: 250 },
{ id: TEMPLATE_IDS[3], type: 'geo', x: 250, y: 250 },
])
},
{ ignoreShapeLock: true }
)
}
return (
<div className="tlui-menu" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 13,
cursor: 'pointer',
userSelect: 'none',
}}
title="When on, left-click and brush selection include locked shapes."
>
<input type="checkbox" checked={selectLocked} onChange={toggleSelectLocked} />
Allow selecting locked shapes
</label>
<TldrawUiButton type="normal" onClick={handleScatter}>
Scatter
</TldrawUiButton>
<TldrawUiButton type="normal" onClick={handleReset}>
Reset
</TldrawUiButton>
</div>
)
}
const components = {
TopPanel: ControlPanel,
}
// [5]
export default function LockedShapesExample() {
return (
<div className="tldraw__editor">
<Tldraw
components={components}
onMount={(editor) => {
// Skip if shapes already exist
if (editor.getShape(TEMPLATE_IDS[0])) {
editor.zoomToFit({ animation: { duration: 0 } })
return
}
// [6] Create locked template shapes
const shapeProps = {
geo: 'rectangle' as const,
w: 130,
h: 130,
dash: 'dashed' as const,
color: 'light-blue' as const,
fill: 'semi' as const,
richText: toRichText('Locked'),
}
editor.createShapes([
{ id: TEMPLATE_IDS[0], type: 'geo', x: 100, y: 100, props: shapeProps },
{ id: TEMPLATE_IDS[1], type: 'geo', x: 250, y: 100, props: shapeProps },
{ id: TEMPLATE_IDS[2], type: 'geo', x: 100, y: 250, props: shapeProps },
{ id: TEMPLATE_IDS[3], type: 'geo', x: 250, y: 250, props: shapeProps },
])
// [7] Lock them immediately
editor.toggleLock(TEMPLATE_IDS)
editor.zoomToFit({ animation: { duration: 0 } })
}}
/>
</div>
)
}
/*
This example demonstrates two ways the editor distinguishes user interaction
from programmatic mutation on locked shapes:
1. `editor.run(fn, { ignoreShapeLock: true })` bypasses the lock guard for
the duration of the callback, so the Scatter / Reset buttons can move
shapes the user can't drag.
2. `editor.options.selectLockedShapes` controls whether locked shapes can be
*selected* (via left-click, brush select, scribble select). The lock
guards on moving, resizing, editing, and deleting still apply — selection
is the only thing this option unlocks.
[1] Pre-defined shape IDs so we can reference them later.
[2] Control panel with the new toggle plus the existing action buttons.
[3] Local React state mirrors the live editor option. The option is
`readonly` at the type level (it's intended as initial editor config) but
the editor stores a single mutable copy that the SelectTool reads on every
relevant pointer event. Mutating the field changes behaviour immediately
without remounting. The cast through `{ selectLockedShapes: boolean }`
isolates the type relaxation to one line.
[4] Both buttons use `editor.run()` with `{ ignoreShapeLock: true }` to
bypass the lock constraint. This option allows programmatic updates even
though user interactions on these shapes are blocked.
[5] The main component sets up the editor.
[6] On mount, we create a 2x2 grid of template shapes.
[7] We immediately lock them with `toggleLock()`. The key behavior: users
cannot move or delete these shapes, but the Scatter / Reset buttons can
still reposition them programmatically.
Try it:
- Default: try left-clicking a template shape — nothing happens (locked
shapes aren't selectable). Right-click still selects.
- Flip the "Allow selecting locked shapes" toggle on, then left-click or
brush-select across a template shape — it gets selected. Try to drag it
by the handles — the lock guard still prevents the move.
- Click Scatter or Reset to see how programmatic updates work with
`ignoreShapeLock: true` regardless of the toggle.
*/
- Locked template — The blue shapes are locked on mount. Try dragging them!
- Scatter/Reset — Uses
editor.run({ ignoreShapeLock: true })to move locked shapes
Is this page helpful?
Prev
Lasso select toolNext
Reactive inputs