tldraw/tldraw
Patterns and conventions
Cross-cutting conventions that apply across the SDK and apps. The repo's AGENTS.md is the authoritative source; this page extracts the patterns most useful to a working contributor.
Reactive state through @tldraw/state
State lives in Atoms and Computeds. UI subscribes via React hooks from @tldraw/state-react.
- Read inside a
Computedto track it:computed('x', () => atom.get()). - Read inside
useValue('x', () => ...)to subscribe a React component. - Batch mutations with
transact(() => { ... }). - Use
react('label', () => { ... })to run a side-effecting reaction. - Don't bypass tracking with
unsafe__withoutCaptureunless you mean it.
AGENTS.md: "Editor state is observable and dependency-tracked. Avoid bypassing existing reactive patterns."
Records and the store
- Document state lives in the store as records. Don't keep parallel state next to it.
- Records are immutable in spirit; mutate through
editor.updateShape(...),store.put(...), oreditor.run(() => { ... }). - Side-effects (
packages/store/src/lib/StoreSideEffects.ts) run before/after store changes — use them for invariant maintenance, not for ad-hoc updates.
Shapes are ShapeUtils
Shape behavior — geometry, rendering, handles, hit-testing, export — lives in a ShapeUtil subclass.
- Base class:
packages/editor/src/lib/editor/shapes/ShapeUtil.ts. - Default shapes:
packages/tldraw/src/lib/shapes/<kind>/<Kind>ShapeUtil.tsx. - Custom shapes follow the same pattern. See
templates/agent/,templates/shader/,templates/workflow/for examples.
AGENTS.md: "Add custom shape behavior through the established ShapeUtil patterns rather than one-off editor patches."
Tools are StateNodes
Tools are StateNode state machines.
- Base class:
packages/editor/src/lib/editor/tools/StateNode.ts. - Default tools:
packages/tldraw/src/lib/tools/<Tool>/. - Complex tools have child states (
Idle,Pointing,Translating, etc.) — seeSelectToolfor the canonical example. - Keep interaction logic close to the state node that owns it.
Bindings are BindingUtils
Shape relationships (e.g., arrows bound to shapes) live in TLBinding records and are managed by BindingUtil classes.
- Base class:
packages/editor/src/lib/editor/bindings/BindingUtil.ts. - Default bindings:
packages/tldraw/src/lib/bindings/. - Update arrows/connections through binding utilities — never by mutating the bound shape from a tool.
Schema discipline
- Schema-affecting changes (new shape kind, new prop, renamed field) need a migration in
packages/tlschema/src/store-migrations.tsor in the shape's record file. - Add a focused test in
packages/tlschema/src/migrations.test.ts. - Run
yarn typecheck— many cross-package type checks live there.
Public API discipline
- Public exports are listed in
packages/<name>/src/index.ts. - Mark types with
@public,@internal, or@betaTSDoc tags.api-extractorreads these to produceapi-report.api.mdfiles. - After changing a public export, run
yarn api-check. Commit the regenerated report.
Style
From AGENTS.md and VOICE.md:
- Sentence case in headings, labels, docs titles, PR titles, issue titles.
- Capitalize proper nouns and code identifiers normally (
PostgreSQL,WebSocket,NodeShapeUtil). - Direct, concrete language. No promotional adjectives.
- No AI attribution in commits, PR descriptions, issues, docs, release notes.
Naming
- Records use the
TLprefix:TLShape,TLBinding,TLAsset,TLPage. - Files for record types match:
TLShape.ts. - Default shape utils:
<Kind>ShapeUtil.tsxin their own folder. - Examples: lowercase kebab-case folders under
apps/examples/src/examples/.
Generated files — do not hand-edit
| File | Owner |
|---|---|
packages/<name>/api-report.api.md |
yarn build-api |
packages/assets/src/... modules |
yarn refresh-assets |
| Translation outputs | yarn build-i18n |
TypeScript build outputs in .tsbuild/ |
yarn build |
If you need to change one of these, run the owning script.
Dependencies
- Workspace-appropriate dependencies only. The SDK packages avoid heavy runtime deps.
- Add new deps with intent —
yarnupdates the lockfile.
What not to do
- Don't import from
@tldraw/editorinternals through a deep path. Use the package entry point. - Don't add
console.logcalls to runtime code paths. The performance budget is real (the editor draws every frame). - Don't introduce bare boolean parameters in new APIs (
AGENTS.md). - Don't add new top-level documentation files unless asked.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.