---
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 `