--- title: v5.0.0 description: Custom theme system, performance improvements, extensibility, attribution, and more. author: tldraw date: 05/06/2026 order: 0 status: published keywords: - changelog - release - v5.0.0 - custom styles - themes - overlays - attribution - mermaid --- This major release introduces a first-class theme system with display values, a new `OverlayUtil` system for canvas overlays, and extensibility APIs for custom record types, asset types, geo shapes, and frame-like shapes. Two new packages ship alongside it: `@tldraw/driver` for imperative editor control and `@tldraw/mermaid` for converting Mermaid diagrams into native shapes. It also adds a shape attribution system with the `TLUserStore` provider, clipboard and performance-measurement hooks, WebSocket hibernation in tlsync, RTL support, cross-window embedding, and various other improvements and bug fixes. ## What's new ### 💥 Custom themes with display values ([#8410](https://github.com/tldraw/tldraw/pull/8410)) A new first-class theme system replaces the previous approach where colors were hardcoded and resolved inline. Themes are 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: ```tsx ``` 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`: ```tsx const MyDrawShapeUtil = DrawShapeUtil.configure({ getCustomDisplayValues(_editor, _shape, _theme, _colorMode) { return { strokeWidth: 10 } }, }) ```
Migration guide The `inferDarkMode` prop has been renamed to `colorScheme` and changed from `boolean` to `'light' | 'dark' | 'system'`: ```tsx // Before // After ``` `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: ```tsx // 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 overlays ([#8469](https://github.com/tldraw/tldraw/pull/8469)) A new `OverlayUtil` system unifies canvas overlays. Built-in brushes, handles, scribbles, snap indicators, selection foreground, collaborator cursors, and arrow hints are now implemented as overlay utils and can be customized or extended via the `overlayUtils` prop. Overlays now render in front of indicators in a unified `CanvasOverlays` layer, and the legacy default component implementations and their CSS have been removed.
Migration guide The legacy overlay components have been removed from `TLEditorComponents`. Canvas overlays are now rendered directly to a 2D canvas context by `OverlayUtil` classes, and customization happens by subclassing the default util rather than swapping a React component. **Customizing a built-in overlay:** extend the default util and give it the same `static type`; it replaces the built-in when passed via `overlayUtils`: ```tsx // Before const components: TLEditorComponents = { Brush: MyBrushComponent, } // After import { Tldraw, BrushOverlayUtil, type TLBrushOverlay } from 'tldraw' class MyBrushOverlayUtil extends BrushOverlayUtil { override render(ctx: CanvasRenderingContext2D, overlays: TLBrushOverlay[]) { const overlay = overlays[0] if (!overlay) return const { x, y, w, h } = overlay.props const zoom = this.editor.getEfficientZoomLevel() ctx.fillStyle = 'rgba(0, 0, 255, 0.1)' ctx.strokeStyle = 'blue' ctx.lineWidth = 1 / zoom ctx.beginPath() ctx.rect(x, y, w, h) ctx.fill() ctx.stroke() } } ``` The following slots have been removed from `TLEditorComponents`. Subclass the matching default overlay util instead: - `Brush`, `ZoomBrush` → `BrushOverlayUtil`, `ZoomBrushOverlayUtil` - `Scribble` → `ScribbleOverlayUtil` - `SnapIndicator` → `SnapIndicatorOverlayUtil` - `Handle`, `Handles` → `ShapeHandleOverlayUtil` - `SelectionForeground`, `SelectionBackground` → `SelectionForegroundOverlayUtil` - `CollaboratorHint` → `CollaboratorHintOverlayUtil` (collaborator cursors/brushes/scribbles have their own utils too) The corresponding `Default*` exports and `LiveCollaborators` have also been removed. **`ShapeUtil.indicator()` → `ShapeUtil.getIndicatorPath()`.** The shape util method that draws a shape's selection indicator no longer returns JSX. It now returns a `Path2D` (or a `TLIndicatorPath`) and is composited onto the canvas overlay layer. Every custom `ShapeUtil` that overrides `indicator` needs to migrate: ```tsx // Before override indicator(shape: MyShape) { return } // After override getIndicatorPath(shape: MyShape): Path2D { const path = new Path2D() path.rect(0, 0, shape.props.w, shape.props.h) return path } ``` For shapes whose indicator should match the shape's geometry, return `path` built from the same primitives. For placeholder shapes (e.g. an `ErrorShape`), return an empty `new Path2D()` and declare the return type explicitly so TypeScript doesn't infer `never`. The React component slots `ShapeIndicator`, `ShapeIndicators`, and `ShapeIndicatorErrorFallback` are also removed from `TLEditorComponents` — there is no replacement React slot, since indicators now render through `getIndicatorPath()`. Overlays cannot render React; they draw into a 2D canvas context. For React-based overlays, render an HTML layer above the canvas via a regular `TLEditorComponents` slot like `InFrontOfTheCanvas`. Overlay colors come from `TLTheme`, not CSS variables. Read them inside `render()` via `this.editor.getCurrentTheme().colors[this.editor.getColorMode()]` so overlays follow light/dark mode automatically. These CSS variables have been removed: `--tl-color-snap`, `--tl-color-brush-fill`, `--tl-color-brush-stroke`, `--tl-color-laser`, `--tl-layer-overlays-custom`. Overriding them (or the removed selectors `.tl-brush`, `.tl-scribble`, `.tl-snap-indicator`, `.tl-handle*`, `.tl-selection__fg__outline`, `.tl-corner-handle`, `.tl-text-handle`, `.tl-corner-crop-handle`, `.tl-mobile-rotate__*`) no longer has any effect. Port these to `TLTheme` entries or to the overlay util's `render()`.
### 💥 Tldraw component options Several `` props that previously lived at the top level have been consolidated under the `options` prop, and a few extension points moved off the component entirely.
Migration guide **`cameraOptions`, `textOptions`, and `deepLinks` props moved into `options`:** ```tsx // Before // After ``` **`embeds` prop removed; configure embed definitions on `EmbedShapeUtil`:** ```tsx // Before // After import { EmbedShapeUtil } from 'tldraw' const ConfiguredEmbedShapeUtil = EmbedShapeUtil.configure({ embedDefinitions: [...DEFAULT_EMBED_DEFINITIONS, customEmbed], }) ``` This silently compiles in some setups (the prop is unknown but JSX won't always reject it), so this is the kind of change that the typecheck won't always catch — search the source for `embeds=` and migrate any matches. **`setDefaultEditorAssetUrls()` and `setDefaultUiAssetUrls()` are no longer part of the public API.** They still exist at runtime for now, but they're marked `@internal` and should not be relied on. Pass `assetUrls` to each `` instead: ```tsx // Before import { setDefaultEditorAssetUrls, setDefaultUiAssetUrls } from 'tldraw' setDefaultEditorAssetUrls(assetUrls) setDefaultUiAssetUrls(assetUrls) // After ``` Do not reach for module augmentation to re-expose the old globals — find each `` mount point and pass `assetUrls` directly.
### Custom record types ([#8213](https://github.com/tldraw/tldraw/pull/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. ```tsx 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. ### Extensible geo shapes ([#8543](https://github.com/tldraw/tldraw/pull/8543)) `GeoShapeUtil` now supports custom geo types via a `customGeoTypes` option on `configure()`. Previously, adding a new geo type required forking or monkey-patching `GeoShapeUtil`; now custom types plug into the existing system and inherit labels, resizing, fill/dash/color styling, SVG export, and hyperlink support — while supplying their own path geometry, snap behavior, creation size, and style panel icon. ```tsx import { GeoShapeUtil, PathBuilder } from 'tldraw' const MyGeoShapeUtil = GeoShapeUtil.configure({ customGeoTypes: { 'rounded-rect': { getPath: (w, h, shape) => { const r = Math.min(w, h) * 0.2 return new PathBuilder() .moveTo(r, 0, { geometry: { isFilled: shape.props.fill !== 'none' } }) .lineTo(w - r, 0) .circularArcTo(r, false, true, w, r) .lineTo(w, h - r) .circularArcTo(r, false, true, w - r, h) .lineTo(r, h) .circularArcTo(r, false, true, 0, h - r) .lineTo(0, r) .circularArcTo(r, false, true, r, 0) .close() }, snapType: 'polygon', icon: 'geo-rectangle', defaultSize: { w: 200, h: 150 }, }, }, }) ``` Each definition supplies `getPath`, `snapType` (`'polygon'` for vertex + center snaps, `'blobby'` for center-only), and an `icon` for the style panel, plus optional `defaultSize` and `onDoubleClick` handler. Custom types appear alongside the built-ins in the geo picker. ### 💥 Extensible asset types ([#8031](https://github.com/tldraw/tldraw/pull/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. ```tsx import { AssetUtil } from '@tldraw/editor' class AudioAssetUtil extends AssetUtil { static override type = 'audio' as const override getSupportedMimeTypes() { return ['audio/mpeg', 'audio/wav'] } override async getAssetFromFile(editor, file) { /* ... */ } } ; ``` `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.
### Frame-like custom shapes ([#8331](https://github.com/tldraw/tldraw/pull/8331)) Custom shapes can now opt into frame-like behavior: clipping children, acting as a parent on paste and drag-in, blocking erasure from inside, and supporting full-brush selection. Previously, frame behavior was hardcoded to the built-in `frame` type; the editor and tools now route frame checks through `editor.getShapeUtil(shape).isFrameLike(shape)`. The easiest way to build one is to extend the new `BaseFrameLikeShapeUtil` abstract class, which provides sensible defaults for `isFrameLike`, `providesBackgroundForChildren`, `canReceiveNewChildrenOfType`, `canRemoveChildrenOfType`, `getClipPath`, `onDragShapesIn`, and `onDragShapesOut`: ```tsx import { BaseFrameLikeShapeUtil, SVGContainer } from '@tldraw/editor' class MyContainerUtil extends BaseFrameLikeShapeUtil { static override type = 'my-container' as const static override props = myContainerShapeProps override getDefaultProps() { return { w: 300, h: 200 } } override component(shape: MyContainerShape) { return ... } override indicator(shape: MyContainerShape) { return } } ``` Shapes that need frame behavior without inheriting from `BaseFrameLikeShapeUtil` can override `isFrameLike()` directly on any `ShapeUtil`. `FrameShapeUtil` now extends `BaseFrameLikeShapeUtil`, so existing frames are unaffected. ### Shape attribution and TLUserStore ([#8147](https://github.com/tldraw/tldraw/pull/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. ```tsx 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`: ```tsx 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.
Migration guide **`useTldrawUser` has been removed.** Previously it bundled user *preferences* (color, color scheme) and user *identity* (id, name) into a single object. These are now separate concerns: - **Preferences** (color, color scheme, snap mode, etc.) are managed via `TLUserPreferences`. Set them with the editor's user-preferences API directly. - **Identity** for attribution comes from a `TLUserStore` provider passed to the new `users` prop on ``. ```tsx // Before const user = useTldrawUser({ userPreferences, setUserPreferences }) // After currentUserSignal, resolve: (userId) => resolvedUserSignal(userId), }} colorScheme={userPreferences.colorScheme} /> ``` If you only need preferences (no custom identity), pass `colorScheme` directly and seed preferences imperatively after mount; if you only need identity, pass `users`. Most apps want both.
### @tldraw/driver ([#7952](https://github.com/tldraw/tldraw/pull/7952)) A new `@tldraw/driver` package provides an imperative API for driving the tldraw editor programmatically. `Driver` wraps an `Editor` instance and exposes event dispatch, selection transforms, clipboard operations, and shape queries with fluent chaining. ```tsx import { Driver } from '@tldraw/driver' const driver = new Driver(editor) driver.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 that demonstrates the package. ### @tldraw/mermaid ([#8194](https://github.com/tldraw/tldraw/pull/8194), [#8285](https://github.com/tldraw/tldraw/pull/8285), [#8322](https://github.com/tldraw/tldraw/pull/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. ```tsx 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. ### Arbitrary iframe embeds ([#8306](https://github.com/tldraw/tldraw/pull/8306)) Paste any `