Next release

This release adds a first-class theme system with display values for customizing default shapes, extensible asset types via a new AssetUtil system, shape attribution with a new TLUserStore provider and extensible user records, clipboard hooks for intercepting copy, cut, and paste, custom record types to the store, a new @tldraw/mermaid package for converting Mermaid diagrams to native shapes, WebSocket hibernation support for tlsync, a new @tldraw/editor-controller package for scripting and automation, RTL language support in the UI, cross-window embedding support, arbitrary iframe embed pasting, a paste-as-plain-text keyboard shortcut, and smarter export trimming. It also includes various other improvements and bug fixes.

What's new

💥 Theme system with display values (#8410)

A new first-class theme system replaces the previous approach where colors were hardcoded and resolved inline. Themes are now named, registered objects that shape utils consume via a structured display values pipeline.

Register custom themes via TLThemes module augmentation for type-safe IDs, add or remove palette colors via TLThemeDefaultColors and TLRemovedDefaultThemeColors, and pass themes to the editor via the themes and initialTheme props:

<Tldraw themes={{ corporate: myCorporateTheme }} initialTheme="corporate" />

Each default shape util defines getDefaultDisplayValues to resolve visual properties (colors, stroke widths, font sizes) from the current theme and color mode. Override display values for a default shape with getCustomDisplayValues:

const MyDrawShapeUtil = DrawShapeUtil.configure({
	getCustomDisplayValues(_editor, _shape, _theme, _colorMode) {
		return { strokeWidth: 10 }
	},
})

New editor methods include getCurrentTheme(), setCurrentTheme(), getThemes(), updateTheme(), updateThemes(), and getColorMode().

Migration guide

The inferDarkMode prop has been renamed to colorScheme and changed from boolean to 'light' | 'dark' | 'system':

// Before
<Tldraw inferDarkMode />

// After
<Tldraw colorScheme="system" />

useIsDarkMode() has been renamed to useColorMode() and returns 'dark' | 'light' instead of boolean.

getDefaultColorTheme() and DefaultColorThemePalette have been removed. Use editor.getCurrentTheme().colors[colorMode] instead:

// Before
const theme = getDefaultColorTheme({ isDarkMode })

// After
const theme = editor.getCurrentTheme()
const colors = theme.colors[editor.getColorMode()]

useDefaultColorTheme() has been removed. Use editor.getCurrentTheme() and useColorMode() instead.

FONT_FAMILIES, FONT_SIZES, LABEL_FONT_SIZES, STROKE_SIZES, TEXT_PROPS, and ARROW_LABEL_FONT_SIZES have been removed — these are now resolved via display values.

SvgExportContext.themeId has been renamed to SvgExportContext.colorMode and changed from string to 'light' | 'dark'.

getColorValue() now takes TLThemeColors as its first argument instead of TLDefaultColorTheme.

Custom record types (#8213)

You can now register custom record types in the tldraw store for persisting and synchronizing domain-specific data that doesn't fit into shapes, bindings, or assets. Custom records support scoping (document/session/presence), validation, migrations, and default properties.

import { createTLSchema, createCustomRecordId } from 'tldraw'

const schema = createTLSchema({
	records: [
		{
			typeName: 'marker',
			scope: 'document',
			validator: markerValidator,
		},
	],
})

TypeScript module augmentation via TLGlobalRecordPropsMap lets custom record types participate in the TLRecord union.

WebSocket hibernation in tlsync (#8070)

TLSocketRoom now supports session resume and snapshot APIs for WebSocket hibernation environments like Cloudflare Durable Objects. Sessions can be suspended and restored without losing state, and the sync-cloudflare template has been updated to use the WebSocket Hibernation API.

New APIs include handleSocketResume() for restoring sessions from snapshots, getSessionSnapshot() for capturing session state, and an onSessionSnapshot callback for persisting snapshots to WebSocket attachments.

@tldraw/editor-controller (#7952)

A new @tldraw/editor-controller package provides an imperative API for driving the tldraw editor programmatically. EditorController wraps an Editor instance and exposes event dispatch, selection transforms, clipboard operations, and shape queries with fluent chaining.

import { EditorController } from '@tldraw/editor-controller'

const controller = new EditorController(editor)
controller.pointerMove(100, 100).pointerDown().pointerMove(200, 200).pointerUp()

This is useful for scripting, automation, agent workflows, and REPL-style interaction. The release includes a Scripter example demonstrating the package.

RTL support (#8033)

The tldraw UI now supports right-to-left languages like Arabic. A new useDirection() hook returns 'ltr' or 'rtl' from the current translation context, and all Radix UI components and CSS have been updated to respect text direction. The dir attribute is set on .tl-container, and CSS uses logical properties (margin-inline-start, inset-inline-end, etc.) instead of physical ones.

@tldraw/mermaid (#8194, #8285, #8322)

A new @tldraw/mermaid package converts Mermaid diagram syntax into native tldraw shapes. Paste Mermaid text to create flowcharts, sequence diagrams, state diagrams, and mind maps as editable shapes on the canvas.

import { createMermaidDiagram } from '@tldraw/mermaid'

await createMermaidDiagram(
	editor,
	`
  graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action]
    B -->|No| D[End]
`
)

The package parses Mermaid syntax, extracts layout from the rendered SVG, and produces a diagram-agnostic blueprint that gets rendered into geo shapes, arrows, and groups.

Node creation is extensible: pass mapNodeToRenderSpec per diagram type to map diagram nodes to different shapes, or use createShape in BlueprintRenderingOptions to take full control of how nodes are created on the canvas.

Clipboard hooks (#8290)

New TldrawOptions hooks let you intercept and customize clipboard copy, cut, and paste. onBeforeCopyToClipboard filters or transforms serialized content before it hits the clipboard, onBeforePasteFromClipboard filters parsed paste payloads before shapes are created, and onClipboardPasteRaw handles raw clipboard data before tldraw's default paste pipeline.

const options: Partial<TldrawOptions> = {
	onBeforeCopyToClipboard(info, content) {
		// filter shapes or transform content before copy/cut
		return content
	},
	onBeforePasteFromClipboard(info, content) {
		// filter or transform parsed paste content
		return content
	},
	onClipboardPasteRaw(info) {
		// handle raw clipboard data yourself; return false to cancel tldraw handling
		return false
	},
}

Shape attribution and TLUserStore (#8147)

A new shape attribution system tracks who created and last edited shapes. The TLUserStore provider interface connects tldraw to your auth system with reactive Signal-based methods: getCurrentUser() returns the active user for presence and attribution, while resolve(userId) resolves any user ID to display info.

<Tldraw
	users={{
		getCurrentUser: () => currentUserSignal,
		resolve: (userId) => resolvedUserSignal(userId),
	}}
/>

User records are now document-scoped via the unified TLUser record type. SDK users can extend user records with custom validated metadata through createTLSchema:

const schema = createTLSchema({
	user: {
		meta: {
			isAdmin: T.boolean,
			department: T.string,
		},
	},
})

Note shapes now track and display a "first edited by" attribution label in the bottom-right corner, showing who first added text to the note.

Arbitrary iframe embeds (#8306)

Paste any <iframe> embed code onto the canvas to create an embed shape. Previously only URLs matching known providers (YouTube, Google Maps, etc.) worked. Now any valid iframe with an HTTP(S) source creates an embed directly, supporting services like OpenStreetMap, SoundCloud, Loom, and more.

💥 Extensible asset types (#8031)

A new AssetUtil base class follows the ShapeUtil / BindingUtil pattern, making the asset system extensible. Previously, assets were a hardcoded union of image, video, and bookmark types. Now you can register custom asset types with their own MIME type handling, file-to-asset conversion, and shape creation logic.

import { AssetUtil } from '@tldraw/editor'

class AudioAssetUtil extends AssetUtil<TLAudioAsset> {
	static override type = 'audio' as const
	override getSupportedMimeTypes() {
		return ['audio/mpeg', 'audio/wav']
	}
	override async getAssetFromFile(editor, file) {
		/* ... */
	}
}

;<Tldraw assetUtils={[AudioAssetUtil]} />

TLAssetStore keeps upload/resolve/remove as cross-cutting concerns, while AssetUtil handles type-specific behavior: MIME types, file-to-asset metadata, and asset-to-shape creation.

Migration guide

assetValidator has been removed. Use imageAssetValidator, videoAssetValidator, or bookmarkAssetValidator instead.

getMediaAssetInfoPartial has been removed. Use AssetUtil.getAssetFromFile instead.

notifyIfFileNotAllowed signature changed from (file, options) to (editor, file, options).

getAssetInfo signature changed from (file, options, assetId?) to (editor, file, assetId?) and now returns TLAsset | null instead of throwing.

Paste as plain text (#8347)

A new Cmd+Shift+V / Ctrl+Shift+V shortcut pastes clipboard content as plain text, stripping HTML and rich formatting. This is the standard shortcut in most apps.

Note: Cmd+Shift+V previously toggled paste-at-cursor positioning. Since the "Paste at cursor" preference now covers that use case, this shortcut has been repurposed for plain text paste. Users who relied on Cmd+Shift+V for paste-at-cursor should use the preference toggle instead.

Cross-window embedding support (#8196)

Tldraw now works correctly when embedded in iframes, Electron pop-out windows, and Obsidian plugins where the global document and window differ from the ones tldraw is mounted in. All bare document and window references have been replaced with container-aware alternatives.

New helpers getOwnerDocument() and getOwnerWindow() are exported from @tldraw/editor, along with Editor.getContainerDocument() and Editor.getContainerWindow() convenience methods.

API changes

  • 💥 Remove defaultColorNames, DefaultColorThemePalette, DefaultLabelColorStyle, TLDefaultColorTheme type, and getDefaultColorTheme() from @tldraw/tlschema. Use editor.getCurrentTheme().colors instead. (#8410)
  • 💥 Remove ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, FONT_SIZES, LABEL_FONT_SIZES, STROKE_SIZES, TEXT_PROPS, and useDefaultColorTheme() from @tldraw/tldraw. These are now resolved via display values. (#8410)
  • 💥 Rename inferDarkMode prop to colorScheme (boolean'light' | 'dark' | 'system'). Rename useIsDarkMode() to useColorMode() (returns 'dark' | 'light' instead of boolean). (#8410)
  • 💥 Rename SvgExportContext.themeId to SvgExportContext.colorMode (string'light' | 'dark'). (#8410)
  • 💥 Change getColorValue() first argument from TLDefaultColorTheme to TLThemeColors. (#8410)
  • 💥 Change PlainTextLabelProps and RichTextLabelProps: fontfontFamily, aligntextAlign, fill removed. (#8410)
  • 💥 Remove assetValidator. Use imageAssetValidator, videoAssetValidator, or bookmarkAssetValidator instead. (#8031)
  • 💥 Remove getMediaAssetInfoPartial. Use AssetUtil.getAssetFromFile instead. (#8031)
  • 💥 Change notifyIfFileNotAllowed signature from (file, options) to (editor, file, options). (#8031)
  • 💥 Change getAssetInfo signature from (file, options, assetId?) to (editor, file, assetId?) and return TLAsset | null instead of throwing. (#8031)
  • Add TLTheme, TLThemeId, TLThemes, TLThemeDefaultColors, TLThemeColors, TLRemovedDefaultThemeColors, ThemeManager, getDisplayValues(), getColorValue(), and DEFAULT_THEME for the new theme system. (#8410)
  • Add themes and initialTheme props to <Tldraw> and <TldrawEditor>. (#8410)
  • Add getCurrentTheme(), setCurrentTheme(), getThemes(), getTheme(), updateTheme(), updateThemes(), and getColorMode() to the editor. (#8410)
  • Add getDefaultDisplayValues and getCustomDisplayValues to shape util options for theme-aware visual properties. (#8410)
  • Add support for pasting arbitrary <iframe> embed codes to create embed shapes from any service. (#8306)
  • Add onBeforeCopyToClipboard, onBeforePasteFromClipboard, and onClipboardPasteRaw hooks to TldrawOptions for intercepting clipboard operations. Add TLClipboardWriteInfo and TLClipboardPasteRawInfo types. Export handleNativeOrMenuCopy from @tldraw/tldraw. (#8290)
  • Add CustomRecordInfo interface, createCustomRecordId(), createCustomRecordMigrationIds(), createCustomRecordMigrationSequence(), isCustomRecord(), isCustomRecordId() for custom record types. createTLSchema() and createTLStore() now accept a records option. (#8213)
  • Add @tldraw/editor-controller package with EditorController class for imperative editor control. (#7952)
  • Add handleSocketResume(), getSessionSnapshot(), and onSessionSnapshot to TLSocketRoom for WebSocket hibernation support. Add clientTimeout option to TLSyncRoom. (#8070)
  • Add Editor.getContainerDocument() and Editor.getContainerWindow() methods, and getOwnerDocument() / getOwnerWindow() helpers for cross-window embedding. (#8196)
  • Add useDirection() hook for RTL support. (#8033)
  • Add 'json' to TLCopyType for copying shapes as JSON in debug mode. (#8206)
  • Change TLSvgExportOptions.padding to accept number | 'auto'. The 'auto' mode (now default) renders with padding then trims to visual content bounds. (#8202)
  • Change Vec.PointsBetween() to accept an optional ease parameter. (#7977)
  • Add @tldraw/mermaid package with createMermaidDiagram(), renderBlueprint(), and MermaidDiagramError for converting Mermaid syntax to tldraw shapes. (#8194)
  • Add mapNodeToRenderSpec per-diagram-type option and createShape override to @tldraw/mermaid for customizing how diagram nodes are rendered as shapes. (#8322)
  • Add TextManager.measureHtmlBatch() for batched DOM text measurement. (#7949)
  • Add TLUserStore interface with getCurrentUser() and resolve() for connecting tldraw to auth systems. Add unified TLUser record type, UserRecordType, createUserId, isUserId, userIdValidator, and createUserRecordType() for extensible user schemas. Add user parameter to createTLSchema(). Add Editor.getAttributionUser(), Editor.getAttributionUserId(), and Editor.getAttributionDisplayName(). Add textFirstEditedBy prop to TLNoteShapeProps. (#8147)
  • Add AssetUtil base class with configure(), getDefaultProps(), getSupportedMimeTypes(), getAssetFromFile(), and createShape(). Add assetUtils prop to <Tldraw>. Add Editor.getAssetUtil(), Editor.hasAssetUtil(), and Editor.getAssetUtilForMimeType(). Add TLGlobalAssetPropsMap for type-safe custom asset registration. Add createAssetRecordType(), defaultAssetSchemas, and assets parameter to createTLSchema(). (#8031)
  • Add Cmd+Shift+V / Ctrl+Shift+V shortcut to paste clipboard content as plain text. Cmd+Shift+V no longer toggles paste-at-cursor positioning. (#8347)

Improvements

  • Optimize geometry hot paths for hit testing: reduce allocations and function call overhead in Vec, Edge2d, Circle2d, Arc2d, Polyline2d, and intersection routines. Circle hit testing is up to 19x faster, polyline nearest-point is 6.8x faster. (#8210)
  • Exports now automatically trim to visual content bounds, capturing overflow like thick strokes and arrowheads without extra whitespace. (#8202)
  • Improve resize performance for multiple geo shapes with text labels by batching DOM measurements into a single pass per frame. (#7949)
  • Allow Cmd+click / Ctrl+click on style panel items to apply style changes only to selected shapes without updating defaults for future shapes. (#8452)
  • Hide selection overlay when nudging shapes with arrow keys. (#8447)
  • Replace @use-gesture/react dependency with custom gesture handling, reducing bundle size and eliminating a stale dependency. (#8392)
  • Tighten iframe referrer policy for embeds to send only the origin instead of the full URL to third-party embed providers. (#8412)
  • Move the debug mode toggle into the preferences submenu. (#8259)
  • Update hotkeys-js keyboard shortcut library from v3 to v4, picking up TypeScript rewrite and keyboard layout handling improvements. (#8372)

Bug fixes

  • Fix bailToMark silently discarding pending history changes when the target mark doesn't exist. (#8260)
  • Fix FocusManager.dispose() not actually removing document event listeners due to .bind() creating new function references. (#8232)
  • Fix pasting into editable text shapes when the clipboard contains tldraw data. (#8192)
  • Fix eraser not erasing shapes when starting a drag from inside a group's bounds. (#8054)
  • Fix over-softened corners and end artifacts when shift-clicking to draw straight line segments. (#7977)
  • Fix draw-shape loop-closing sensitivity so closing works more consistently across zoom levels. (#8293)
  • Fix "back to content" button flickering when both it and the "move focus to canvas" button are visible. (#8334) (contributed by @kaneel)
  • Fix slight positioning drift when pasting text onto the canvas. (#8345)
  • Fix missing sandbox attribute on GitHub Gist embeds. (#8403)
  • Restrict sandbox permissions for unknown/arbitrary embeds to mitigate security risks from untrusted content. (#8404)
  • Fix leaked camera animations, following subscriptions, and stale menu state when the editor is disposed during active operations. (#8422)
  • Fix right-clicking inside a multi-selection over a filled background shape changing the selection. (#8434)
  • Fix memory leak where the Editor was retained after unmount via a shared throttled updateHoveredShapeId closure. (#8439)
  • Fix pattern fill not scaling correctly with dynamic size mode. (#8441)
  • Fix arrow labels rendered at incorrect size when editing at high zoom in dynamic size mode. (#8451)
  • Fix shadow artifact and oversized caret on notes and geo shapes at high zoom in dynamic size mode. (#8378)
  • Fix crash from duplicate fractional index keys in kickoutOccludedShapes during multiplayer sync. (#8448)
  • Fix NaN propagation from zero-length labeled arrows causing all shapes to disappear. (#8329)
  • Fix crash when isolating curved arrows with degenerate geometry. (#8176)
  • Fix camera state stuck at 'moving' on dispose, blocking shape interactions on the next editor instance. (#8396)
  • Fix spiky artifacts on draw-style geometric shapes by using a circular random offset distribution instead of a square one. (#8466)
  • Fix opacity slider drag not working on Safari due to stopPropagation blocking pointer events. (#8519)
Prev
v4.2.0
Next
v4.3.0