Open-Source Wikis

/

tldraw

/

How to contribute

/

Patterns and conventions

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 Computed to 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__withoutCapture unless 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(...), or editor.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.) — see SelectTool for 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.ts or 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 @beta TSDoc tags. api-extractor reads these to produce api-report.api.md files.
  • 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 TL prefix: TLShape, TLBinding, TLAsset, TLPage.
  • Files for record types match: TLShape.ts.
  • Default shape utils: <Kind>ShapeUtil.tsx in 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 — yarn updates the lockfile.

What not to do

  • Don't import from @tldraw/editor internals through a deep path. Use the package entry point.
  • Don't add console.log calls 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.

Patterns and conventions – tldraw wiki | Factory