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

318 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: v4.3.0
description: Examples
author: tldraw
date: 1/21/2026
order: 0
status: published
---
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.3.0)
This release introduces several significant changes: a new pattern for defining custom shape/binding typings, pluggable storage for `TLSocketRoom` with a new SQLite option, reactive `editor.inputs`, and optimized draw shape encoding. It also adds various other API improvements, performance optimizations, and bug fixes, including better support for React 19.
---
### 💥 New pattern for defining custom shape/binding types ([#7091](https://github.com/tldraw/tldraw/pull/7091))
We've improved the developer experience of working with custom shape and binding types. There's now less boilerplate and fewer gotchas when using tldraw APIs in a type-safe manner.
This is a minor breaking change at the type level—your code will still run, but you'll get TypeScript errors until you migrate.
<details>
<summary>Migration guide</summary>
When _declaring_ types for custom shapes, you can now use TypeScript's module augmentation feature to provide more specific types for the custom shape.
Before:
```ts
import { TLBaseShape } from 'tldraw'
// Shapes were defined by using the helper TLBaseShape type
type MyShape = TLBaseShape`<'my-shape', { w: number; h: number; text: string }>`
```
After:
```ts
import { TLShape } from 'tldraw'
const MY_SHAPE = 'my-shape'
// We now use TypeScript's module augmentation feature to allow
// extending the builtin TLShape type.
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[MY_SHAPE]: { w: number; h: number; text: string }
}
}
type MyShape = TLShape`<typeof MY_SHAPE>`
```
The benefit of this new system is that Editor APIs such as `createShape` now know about your custom shapes automatically:
```ts
// Just works - TypeScript validates props and provides autocomplete
editor.createShape({ type: 'my-shape', props: { w: 100, h: 100, text: 'Hello' } })
// Will cause a TypeScript error for `text`
editor.createShape({ type: 'my-shape', props: { w: 100, h: 100, text: 123 } })
```
The same pattern applies to custom bindings — augment `TLGlobalBindingPropsMap` and use `TLBinding<'binding-name'>`:
```ts
declare module 'tldraw' {
export interface TLGlobalBindingPropsMap {
'my-binding': { offset: number }
}
}
type MyBinding = TLBinding`<'my-binding'>`
```
See the [Custom Shapes Guide](https://tldraw.dev/docs/shapes#Custom-shapes) and the [Pin Bindings example](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/shapes/tools/pin-bindings) for details.
**After registering, remove the `TLBaseShape` import** — it's no longer used and will trip lint rules that flag unused imports.
**Shape type names are global.** `TLGlobalShapePropsMap` is a single shared registry, so two custom shapes that previously used the same type name in different files now collide. Pick a unique name for each, and remember the rename ripples beyond the type alias:
- `static override type = '...'` on the `ShapeUtil` subclass
- Every `editor.createShape({ type: '...' })` and `editor.updateShape({ type: '...' })` call site
- The `TLGlobalShapePropsMap` entry itself
- Any persisted snapshot or migration JSON that references the old name
**Use `as const` on the static `type` field.** TypeScript may widen `static override type = 'my-shape'` to `string`, which fails the new constraint. Add `as const` to keep it a string literal:
```ts
class MyShapeUtil extends ShapeUtil`<MyShape>` {
static override type = 'my-shape' as const
// ...
}
```
The same fix applies to `BaseBoxShapeTool` (and other tool) subclasses, which expose `static override shapeType = '...'`. If you see TS2416 on a tool's `shapeType`, add `as const` there too. The two fields are easy to confuse — check both.
**Heterogeneous `createShapes`/`updateShapes` arrays may need a cast.** Because `TLShape` is now a discriminated union over the global props map, a mapped array of mixed shape types no longer narrows automatically. For homogeneous arrays where every item shares one shape type, `as const` on the `type` field in the mapped object literal is enough. For genuinely heterogeneous arrays, go straight to `as TLShapePartial[]` (or `as TLCreateShapePartial[]` for `createShapes`):
```ts
// Heterogeneous update — the union doesn't narrow
editor.updateShapes(
shapes.map((s) => ({ id: s.id, type: s.type, x: s.x + 10 })) as TLShapePartial[]
)
```
Don't reach for `satisfies TLShapePartial` here — `TLShapePartial` is distributive over the `TLShape` union, so `satisfies` enforces a literal-per-variant constraint that mapped-over heterogeneous shapes can't meet. The cast is the right tool for this specific shape of code.
</details>
(contributed by [@Andarist](https://github.com/Andarist))
---
### Pluggable storage for TLSocketRoom + SQLite support ([#7320](https://github.com/tldraw/tldraw/pull/7320), [#7123](https://github.com/tldraw/tldraw/pull/7123))
We've refactored the `TLSocketRoom` API to support a pluggable storage layer. We're providing two implementations:
- **`SQLiteSyncStorage`** – Automatically persists room state to SQLite. Recommended for production.
- **`InMemorySyncStorage`** – Keeps state in memory with manual persistence via callbacks (previous built-in behavior).
**We recommend switching to `SQLiteSyncStorage` if your environment supports SQLite** (Cloudflare Durable Objects, Node.js, Bun, Deno). It provides automatic persistence, lower memory usage, and faster startup times.
<details>
<summary>Why SQLite?</summary>
- **Automatic persistence**: Data survives process restarts without manual snapshot handling
- **Lower memory usage**: No need to keep entire documents in memory
- **Faster startup**: No need to load the document into memory before accepting socket connections
- **Simpler code**: No more `onChange` callbacks and manual persistence logic
</details>
<details>
<summary>Platform support</summary>
| Platform | Wrapper | SQLite Library |
| -------------------------- | -------------------------------- | --------------------------------- |
| Cloudflare Durable Objects | `DurableObjectSqliteSyncWrapper` | Built-in `ctx.storage` |
| Node.js/Deno | `NodeSqliteWrapper` | `better-sqlite3` or `node:sqlite` |
See the [Cloudflare template](https://github.com/tldraw/tldraw-sync-cloudflare) and the [Node server example](https://github.com/tldraw/tldraw/tree/main/templates/simple-server-example) respectively. Bun support should be straightforward to add.
</details>
<details>
<summary>Migration guide</summary>
Existing code continues to work, however we have deprecated the following `TLSocketRoom` options:
- `initialSnapshot`
- `onDataChange`
These are replaced by the new `storage` option. We've also deprecated the `TLSocketRoom.updateStore` method, which has been supplanted by `storage.transaction`.
Before:
```ts
const existingSnapshot = loadExistingSnapshot()
const room = new TLSocketRoom({
initialSnapshot: existingSnapshot,
onDataChange: () => {
persistSnapshot(room.getCurrentSnapshot())
},
})
```
If you want to keep the same behavior with in-memory document storage and manual persistence:
```ts
import { InMemorySyncStorage, TLSocketRoom } from '@tldraw/sync-core'
const room = new TLSocketRoom({
storage: new InMemorySyncStorage({
snapshot: existingSnapshot,
onChange() {
saveToDatabase(storage.getSnapshot())
},
}),
})
```
However, we recommend switching to SQLite. Users of our Cloudflare template should follow the migration guide on the [sync docs page](https://tldraw.dev/docs/sync).
If you're using `TLSocketRoom` on Node, creating the room should end up looking something like this:
```ts
import Database from 'better-sqlite3'
import { SQLiteSyncStorage, NodeSqliteWrapper, TLSocketRoom, RoomSnapshot } from '@tldraw/sync-core'
async function createRoom(roomId: string) {
const db = new Database(`path/to/${roomId}.db`)
const sql = new NodeSqliteWrapper(db)
let snapshot: RoomSnapshot | undefined = undefined
if (!SQLiteSyncStorage.hasBeenInitialized(sql)) {
// This db hasn't been used before, so if it's a pre-existing
// document, load the legacy room snapshot
snapshot = await loadExistingSnapshot()
}
const storage = new SQLiteSyncStorage({ sql, snapshot })
return new TLSocketRoom({
storage,
onSessionRemoved(room, args) {
if (args.numSessionsRemaining === 0) {
room.close()
db.close()
}
},
})
}
```
</details>
---
### Optimized draw shape encoding ([#7364](https://github.com/tldraw/tldraw/pull/7364), [#7710](https://github.com/tldraw/tldraw/pull/7710))
Draw and highlight shape point data is now stored using a compact delta-encoded binary format instead of JSON arrays. This reduces storage size by approximately 80% while preserving stroke fidelity.
<details>
<summary>Breaking change details</summary>
If you were reading or writing draw shape data programmatically you might need to update your code to use the new format.
- `TLDrawShapeSegment.points` renamed to `.path` and changed from `VecModel[]` to `string` (base64-encoded)
- Added `scaleX` and `scaleY` properties to draw and highlight shapes
- New exports: `b64Vecs` encoding utilities, e.g. `getPointsFromDrawSegment` helper. Use this if you need to manually read/write point data.
Existing documents are automatically migrated.
</details>
---
### Reactive inputs ([#7312](https://github.com/tldraw/tldraw/pull/7312))
Refactored `editor.inputs` to use reactive atoms via the new `InputsManager` class. All input state is now accessed via getter methods (e.g., `editor.inputs.getCurrentPagePoint()`, `editor.inputs.getShiftKey()`). Direct property access is deprecated but still supported for backwards compatibility.
## API changes
- 💥 **`DefaultTopPanel`** export removed from `tldraw`. The top panel component for displaying the offline indicator is now handled internally by `PeopleMenu`. ([#7568](https://github.com/tldraw/tldraw/pull/7568))
- 💥 **`TextDirection`** export removed from `tldraw`. Use TipTap's native `TextDirection` extension instead. The `richTextValidator` now includes an optional `attrs` property - a migration may be necessary for older clients/custom shapes. ([#7304](https://github.com/tldraw/tldraw/pull/7304))
- Add `tlenvReactive` atom to `@tldraw/editor` for reactive environment state tracking, including coarse pointer detection that updates when users switch between mouse and touch input. ([#7296](https://github.com/tldraw/tldraw/pull/7296))
- Add `hideAllTooltips()` helper function for programmatically dismissing tooltips. ([#7288](https://github.com/tldraw/tldraw/pull/7288))
- Add `zoomToFitPadding` option to `TldrawOptions` to customize the default padding used by zoom-to-fit operations. ([#7602](https://github.com/tldraw/tldraw/pull/7602))
- Add `snapThreshold` option to `TldrawOptions` for configuring the snap distance, defaulting to 8 screen pixels. ([#7543](https://github.com/tldraw/tldraw/pull/7543))
- Add `resizeChildren` configuration option to `FrameShapeUtil` to allow frame children to be resized proportionally when the frame is resized. ([#7526](https://github.com/tldraw/tldraw/pull/7526))
- Add `Editor.canEditShape()` and `Editor.canCropShape()` methods to centralize shape permission checks. Add `ShapeUtil.canEditWhileLocked()` for shapes that remain editable when locked. ([#7361](https://github.com/tldraw/tldraw/pull/7361))
- Add `editor.getDebouncedZoomLevel()` and `editor.getEfficientZoomLevel()` methods for improved zoom performance on dense canvases. Add `debouncedZoom` and `debouncedZoomThreshold` options. ([#7235](https://github.com/tldraw/tldraw/pull/7235))
- Add configurable `showTextOutline` option to `TextShapeUtil`, `ArrowShapeUtil`, and `GeoShapeUtil` via `.configure()` pattern. ([#7314](https://github.com/tldraw/tldraw/pull/7314))
- Export freehand stroke utilities: `getStroke`, `getStrokeOutlinePoints`, and `setStrokePointRadii`. ([#7400](https://github.com/tldraw/tldraw/pull/7400)) (contributed by [@VimHax](https://github.com/VimHax))
- Add `Box.isValid()` method to check for finite coordinates. ([#7532](https://github.com/tldraw/tldraw/pull/7532))
- Add `spacebarPanning` option to control whether spacebar activates pan mode. ([#7312](https://github.com/tldraw/tldraw/pull/7312))
- Introduce pluggable `TLSyncStorage` API for `TLSocketRoom`. The `initialSnapshot` and `onDataChange` options are now deprecated in favor of the new `storage` option. ([#7123](https://github.com/tldraw/tldraw/pull/7123))
- Export `DefaultLabelColorStyle` which is necessary for rich text in custom shapes. ([#7114](https://github.com/tldraw/tldraw/pull/7114))
### Improvements
- Improve coarse pointer detection by replacing CSS media queries with a reactive `data-coarse` attribute that updates when users switch between mouse and touch input. ([#7404](https://github.com/tldraw/tldraw/pull/7404))
- Add CSS containment to main toolbar and text measurement element for improved rendering performance. ([#7406](https://github.com/tldraw/tldraw/pull/7406)) ([#7407](https://github.com/tldraw/tldraw/pull/7407))
- Improve cross-realm support by scoping canvas event listeners to the editor container's ownerDocument. ([#7113](https://github.com/tldraw/tldraw/pull/7113))
- Simplify ImmutableMap implementation for better code clarity. ([#7431](https://github.com/tldraw/tldraw/pull/7431))
- Improve code readability in number validator by using `Number.isFinite()` instead of arithmetic trick. ([#7374](https://github.com/tldraw/tldraw/pull/7374))
- Upgrade to React 19 with all necessary type and configuration changes for compatibility. ([#7317](https://github.com/tldraw/tldraw/pull/7317))
- Improve signal graph traversal performance by eliminating per-recursion closure allocations. ([#7430](https://github.com/tldraw/tldraw/pull/7430))
- Optimize object utility functions. ([#7432](https://github.com/tldraw/tldraw/pull/7432))
- Use in-place sorting in parentsToChildren derivation. ([#7433](https://github.com/tldraw/tldraw/pull/7433))
- Optimize notVisibleShapes derivation with inlined bounds checks. ([#7429](https://github.com/tldraw/tldraw/pull/7429))
- Cache label size measurements with WeakCache for improved geo shape performance. ([#7412](https://github.com/tldraw/tldraw/pull/7412))
- Optimize validators with fast paths and production inlining. ([#7373](https://github.com/tldraw/tldraw/pull/7373))
### Bug fixes
- Fix migrations for draw and highlight shapes to be idempotent, preventing errors when migrations run multiple times. ([#7389](https://github.com/tldraw/tldraw/pull/7389))
- Fix dot detection in draw and highlight shapes after the point compression change. ([#7365](https://github.com/tldraw/tldraw/pull/7365))
- Fix clicking a shape's text label while editing to re-focus the input and select all text. ([#7342](https://github.com/tldraw/tldraw/pull/7342))
- Fix pasting at cursor to correctly account for frames and parent containers. ([#7277](https://github.com/tldraw/tldraw/pull/7277))
- Fix editing mode to exit when dragging causes the text input to blur. ([#7291](https://github.com/tldraw/tldraw/pull/7291))
- Fix CommonJS build issues with TipTap imports in rich text module. ([#7282](https://github.com/tldraw/tldraw/pull/7282))
- Fix iOS automatically zooming in on input fields by ensuring 16px minimum font size. ([#7118](https://github.com/tldraw/tldraw/pull/7118))
- Fix `distanceToLineSegment` returning squared distance instead of actual distance, causing hit testing (eraser, scribble select) to be too strict. ([#7610](https://github.com/tldraw/tldraw/pull/7610)) (contributed by [@arpit-goblins](https://github.com/arpit-goblins))
- Fix `zoomToSelection` to toggle between 100% zoom and zoom-to-fit behavior. ([#7536](https://github.com/tldraw/tldraw/pull/7536))
- Fix export of SVG markers by handling fragment-only URLs correctly. ([#7506](https://github.com/tldraw/tldraw/pull/7506)) (contributed by [@PidgeyBE](https://github.com/PidgeyBE))
- Restore wheel and pinch canvas event emission. ([#6834](https://github.com/tldraw/tldraw/pull/6834)) (contributed by [@swdev33](https://github.com/swdev33))
- Fix context menu submenu flickering when hovered. ([#6837](https://github.com/tldraw/tldraw/pull/6837)) (contributed by [@swdev33](https://github.com/swdev33))
- Fix rotated shape positions not being restored after flipping twice. ([#7359](https://github.com/tldraw/tldraw/pull/7359))
- Fix keyboard shortcuts and clipboard events not working when `hideUi` is true. ([#7367](https://github.com/tldraw/tldraw/pull/7367))
- Fix elbow arrows routing incorrectly when using dynamic sizing at high zoom levels. ([#7424](https://github.com/tldraw/tldraw/pull/7424))
- Allow fullscreen for embed shapes. ([#7417](https://github.com/tldraw/tldraw/pull/7417))
- Move mobile rotate handle to bottom for image and video shapes to accommodate the contextual toolbar. ([#6727](https://github.com/tldraw/tldraw/pull/6727))
- Fix dropdown menu items incorrectly displaying an icon on the right side. ([#7533](https://github.com/tldraw/tldraw/pull/7533))
- Fix zoom menu showing debounced zoom level for canvases with many shapes. ([#7626](https://github.com/tldraw/tldraw/pull/7626))
- Fix extra line appearing after bullet and ordered lists in text shapes. ([#7643](https://github.com/tldraw/tldraw/pull/7643)) (contributed by [@sahiee-dev](https://github.com/sahiee-dev))
- Fix "Back to content" button not appearing when selected shapes are off-screen. ([#7649](https://github.com/tldraw/tldraw/pull/7649))
- Fix dotted freehand lines becoming invisible at minimum zoom on Chrome. ([#7650](https://github.com/tldraw/tldraw/pull/7650))
- Fix menu bar stretching to full width on mobile viewports instead of fitting content. ([#7568](https://github.com/tldraw/tldraw/pull/7568))
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.3.0)
---
## Patch releases
### v4.3.1
- Fix draw shape precision issues at high canvas coordinates by switching to delta encoding with a Float32 anchor point. ([#7710](https://github.com/tldraw/tldraw/pull/7710))
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.3.1)
### v4.3.2
- Fix crash when cropping custom shapes that don't include `isCircle` in their crop schema. ([#7931](https://github.com/tldraw/tldraw/pull/7931))
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.3.2)