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
318 lines
18 KiB
Plaintext
318 lines
18 KiB
Plaintext
---
|
||
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)
|