Locked shapes

Lock shapes against user editing, move them from code with ignoreShapeLock, and let users select them with the selectLockedShapes option.

import { useMemo } from 'react'
import {
	atom,
	createShapeId,
	Editor,
	Tldraw,
	TldrawUiButton,
	TLComponents,
	TLShapeId,
	toRichText,
	useEditor,
	useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './locked-shapes.css'

// There's a guide at the bottom of this file!

const TEMPLATE_IDS: TLShapeId[] = [
	createShapeId('t1'),
	createShapeId('t2'),
	createShapeId('t3'),
	createShapeId('t4'),
]

const HOME_POSITIONS = [
	{ x: 100, y: 100 },
	{ x: 250, y: 100 },
	{ x: 100, y: 250 },
	{ x: 250, y: 250 },
]

// [1]
const selectLockedShapes$ = atom('selectLockedShapes', false)

function ControlPanel() {
	const editor = useEditor()
	const selectLockedShapes = useValue(selectLockedShapes$)

	// [2]
	const moveTemplates = (positions: { x: number; y: number }[]) => {
		editor.run(
			() => {
				editor.updateShapes(TEMPLATE_IDS.map((id, i) => ({ id, type: 'geo', ...positions[i] })))
			},
			{ ignoreShapeLock: true }
		)
	}

	const handleScatter = () => {
		moveTemplates(
			TEMPLATE_IDS.map(() => ({ x: 50 + Math.random() * 300, y: 50 + Math.random() * 300 }))
		)
	}

	return (
		<div className="tlui-menu locked-shapes-panel">
			<label title="When on, left-click and brush selection include locked shapes.">
				<input
					type="checkbox"
					checked={selectLockedShapes}
					onChange={() => selectLockedShapes$.set(!selectLockedShapes)}
				/>
				Allow selecting locked shapes
			</label>
			<TldrawUiButton type="normal" onClick={handleScatter}>
				Scatter
			</TldrawUiButton>
			<TldrawUiButton type="normal" onClick={() => moveTemplates(HOME_POSITIONS)}>
				Reset
			</TldrawUiButton>
		</div>
	)
}

const components: TLComponents = {
	TopPanel: ControlPanel,
}

// [3]
function handleMount(editor: Editor) {
	if (!editor.getShape(TEMPLATE_IDS[0])) {
		const props = {
			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(
			TEMPLATE_IDS.map((id, i) => ({ id, type: 'geo', ...HOME_POSITIONS[i], props }))
		)
		editor.toggleLock(TEMPLATE_IDS)
	}
	editor.zoomToFit({ animation: { duration: 0 } })
}

export default function LockedShapesExample() {
	const selectLockedShapes = useValue(selectLockedShapes$)
	const options = useMemo(() => ({ selectLockedShapes }), [selectLockedShapes])

	return (
		<div className="tldraw__editor">
			<Tldraw components={components} options={options} onMount={handleMount} />
		</div>
	)
}

/*
Locked shapes can't be moved, resized, edited, or deleted by the user. This example shows two ways the
editor lets you work around that:

- `editor.run(fn, { ignoreShapeLock: true })` lifts the lock guard for the duration of the callback, so
  code can move shapes the user can't drag.
- The `selectLockedShapes` editor option lets locked shapes be selected by left-click, brush, and
  scribble selection. Only selection changes; the lock guards still apply to everything else.

Try it: left-click a blue shape and nothing happens (right-click still selects it). Turn on "Allow
selecting locked shapes" and left-click or brush across one; it selects, but the handles won't move it.
Scatter and Reset move the shapes regardless of the toggle.

[1]
The toggle lives in a module-level `atom` so both the control panel (inside the editor) and the example
component (outside it) can read it with `useValue`. Editor options are read-only once the editor is
created, so the example passes a new `options` object to `<Tldraw>`, which recreates the editor with
the new setting. The store is preserved, so the shapes and camera survive the swap.

[2]
Both buttons wrap `updateShapes` in `editor.run` with `ignoreShapeLock: true`. Without it, updates to
locked shapes are silently dropped.

[3]
On mount, create a 2x2 grid of shapes and lock them with `toggleLock`. `onMount` runs again whenever the
editor is recreated, so it checks whether the shapes already exist first.
*/

The blue shapes are locked on mount with editor.toggleLock. Users can't drag, resize, edit, or delete them, and left-clicking one does nothing (right-click still selects it).

  • Scatter / Reset wrap editor.updateShapes in editor.run(fn, { ignoreShapeLock: true }), which lifts the lock guard for the callback so code can move shapes the user can't.
  • Allow selecting locked shapes toggles the selectLockedShapes editor option, which lets locked shapes be picked by left-click, brush, and scribble selection. It only affects selection; the lock guards on moving, resizing, editing, and deleting still apply. Editor options are fixed for the editor's lifetime, so flipping the toggle passes a new options object to <Tldraw> and the editor is recreated (the store, and so the shapes, are kept).
Is this page helpful?
Prev
Lasso select tool
Next
Reactive inputs