# tldraw SDK Examples
--------
# Tldraw component
Category: Getting started
Keywords: basic, intro, simple, quick, start, hello world, installation
The simplest way to use the `Tldraw` component.
The `Tldraw` component provides the tldraw editor as a regular React component. You can put this component anywhere in your React project. In this example, we make the component take up the height and width of the container.
By default, the component does not persist between refreshes or sync locally between tabs. To keep your work after a refresh, check the [persistence key example](https://tldraw.dev/examples/persistence-key).
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function BasicExample() {
return (
)
}
```
--------
# Editor options
Category: Configuration
Keywords:
Override default editor options like max number of pages and animation speed.
Use the `options` property to override tldraw's options. In this example, we limit the maximum number of pages to 3, and slow down camera animations like zoom in and zoom out.
## App.tsx
```tsx
import { Tldraw, TldrawOptions } from 'tldraw'
import 'tldraw/tldraw.css'
const options: Partial = {
maxPages: 3,
animationMediumMs: 5000,
}
export default function CustomOptionsExample() {
return (
)
}
```
--------
# Shape options
Category: Configuration
Keywords: config, configure, shape, util, frame
Change the behavior of built-in shapes by setting their options.
Some of the builtin tldraw shapes can be customized to behave differently based on your needs. This is done via the `ShapeUtil.configure` function which returns a new version of the shape's util class with custom options specified.
You can see a shape's options by looking at the `options` property of its `ShapeUtil`. For example, the note shape's options are listed at [`NoteShapeOptions`](https://tldraw.dev/reference/tldraw/NoteShapeOptions).
## App.tsx
```tsx
import { FrameShapeUtil, NoteShapeUtil, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
const shapeUtils = [
// Enable colors for frame shapes
FrameShapeUtil.configure({ showColors: true }),
// Enable resizing for note shapes
NoteShapeUtil.configure({ resizeMode: 'scale' }),
]
export default function ConfigureShapeUtilExample() {
return (
)
}
```
--------
# Camera options
Category: Configuration
Keywords: api, fixed, constraints, camera bounds, pan speed, zoom speed
Configure the camera's options and constraints.
The `Tldraw` component provides a prop, `cameraOptions`, that can be used to set the camera's constraints, zoom behavior, and other options. This example lets you modify those options to see how they affect the camera's behavior.
## App.tsx
```tsx
import { useEffect } from 'react'
import {
BoxModel,
TLCameraOptions,
Tldraw,
Vec,
clamp,
track,
useEditor,
useLocalStorageState,
} from 'tldraw'
import 'tldraw/tldraw.css'
const CAMERA_OPTIONS: TLCameraOptions = {
isLocked: false,
wheelBehavior: 'pan',
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
constraints: {
initialZoom: 'fit-max',
baseZoom: 'fit-max',
bounds: {
x: 0,
y: 0,
w: 1600,
h: 900,
},
behavior: { x: 'contain', y: 'contain' },
padding: { x: 100, y: 100 },
origin: { x: 0.5, y: 0.5 },
},
}
const BOUNDS_SIZES: Record = {
a4: { x: 0, y: 0, w: 1050, h: 1485 },
landscape: { x: 0, y: 0, w: 1600, h: 900 },
portrait: { x: 0, y: 0, w: 900, h: 1600 },
square: { x: 0, y: 0, w: 900, h: 900 },
}
export default function CameraOptionsExample() {
return (
)
})
```
--------
# Asset options
Category: Configuration
Keywords: images, videos, gif, dimensions, accepted image types
Control the assets (images, videos, etc.) that can be added to the canvas.
This example demonstrates the `` component's props that give you control over assets: which types are allowed, the maximum size, and maximum dimensions.
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function AssetPropsExample() {
return (
)
}
/*
This example shows how to use props on the Tldraw component to control what types of
assets can be uploaded.
*/
```
--------
# Custom text outline
Category: Configuration
Keywords:
Disable text outlines on text and arrow labels.
This example shows how to configure the `ArrowShapeUtil` and `TextShapeUtil` to disable text outlines. By default, tldraw adds a text outline (using the canvas background color) to help text stand out when overlapping with other shapes. You can disable this feature by configuring the shape utilities.
This is particularly useful for:
- Performance optimization on certain browsers (we already skip on Safari)
- Different visual styling preferences
## App.tsx
```tsx
import { ArrowShapeUtil, GeoShapeUtil, TextShapeUtil, Tldraw, toRichText } from 'tldraw'
import 'tldraw/tldraw.css'
// Configure the geo shape to disable outline
const CustomGeoShapeUtil = GeoShapeUtil.configure({
showTextOutline: false,
})
// Configure the arrow shape to disable text outline
const CustomArrowShapeUtil = ArrowShapeUtil.configure({
showTextOutline: false,
})
// Configure the text shape to disable outline
const CustomTextShapeUtil = TextShapeUtil.configure({
showTextOutline: false,
})
// Use the configured shape utilities
const customShapeUtils = [CustomArrowShapeUtil, CustomTextShapeUtil, CustomGeoShapeUtil]
export default function CustomTextOutlineExample() {
return (
{
if (editor.getCurrentPageShapeIds().size > 0) return
const message = toRichText('very good whiteboard')
// Lots of overlapping text shapes. These would normally be differentiated a bit using the text outline!
editor.createShapes([
{
type: 'text',
x: 100,
y: 100,
props: { richText: message },
},
{
type: 'text',
x: 110,
y: 110,
props: { richText: message },
},
{
type: 'text',
x: 120,
y: 120,
props: { richText: message },
},
{
type: 'arrow',
x: 0,
y: 0,
props: {
richText: toRichText('hello world'),
start: { x: 0, y: 0 },
end: { x: 200, y: 200 },
},
},
])
}}
/>
)
}
```
--------
# Persistence key
Category: Configuration
Keywords: local storage
Persist the editor's content between sessions by using a persistence key.
If the `persistenceKey` prop is provided to the `` component, the editor will persist its data locally under that key.
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function PersistenceKeyExample() {
return (
)
}
```
--------
# Read-only
Category: Configuration
Keywords: read, only mode
Use the editor in read-only mode.
This example shows how to make the editor read-only. This will disable all editing
functionality and show only the select tool, hand tool and laser pointer on the
toolbar.
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function ReadOnlyExample() {
return (
)
}
/*
This example shows how to make the editor read-only. We use the `onMount` prop to
set the editor's `isReadonly` state to `true`. This will disable all editing
functionality and show only the select tool, hand tool and laser pointer on the
toolbar.
*/
```
--------
# Reduced motion preferences
Category: Configuration
Keywords: accessibility, a11y, reduced motion, animation, prefers-reduced-motion, custom shape
Respecting user motion preferences in custom shapes.
This example demonstrates how to build custom shapes that respect the user's reduced motion preferences using the `usePrefersReducedMotion()` hook.
The hook checks both the user's tldraw preference (`animationSpeed: 0`) and the OS-level `prefers-reduced-motion` setting. When reduced motion is preferred, animations are replaced with static alternatives.
The example includes:
- A custom shape with animated and static variants
- A toggle button to switch between animation modes
- CSS animations controlled by the motion preference
This is particularly important for accessibility, ensuring users with vestibular disorders or motion sensitivities can use your application comfortably.
## App.tsx
```tsx
import {
Geometry2d,
HTMLContainer,
RecordProps,
Rectangle2d,
ShapeUtil,
T,
TLComponents,
TLShape,
Tldraw,
TldrawUiButton,
track,
useEditor,
usePrefersReducedMotion,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './reduced-motion.css'
const PULSE_SHAPE_TYPE = 'pulse-shape'
// [1]
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[PULSE_SHAPE_TYPE]: { w: number; h: number }
}
}
type PulseShape = TLShape
// [2]
function PulseShapeComponent({ shape: _shape }: { shape: PulseShape }) {
const prefersReducedMotion = usePrefersReducedMotion()
return (
)
}
/*
[1]
Extend TLGlobalShapePropsMap to register our custom shape type with TypeScript. This shape
has width (w) and height (h) properties.
[2]
PulseShapeComponent is a React component that renders the shape's content. It uses
usePrefersReducedMotion() to check if the user prefers reduced motion. This hook returns
true when either:
- The user has set animationSpeed to 0 in tldraw preferences
- The OS has prefers-reduced-motion enabled
[3]
The visual indicator changes based on motion preference. When reduced motion is preferred,
it shows a static gray circle. Otherwise, it shows an animated blue circle with a pulsing
effect defined in the CSS file.
[4]
PulseShapeUtil class controls the shape's behavior. The component method returns the
PulseShapeComponent, which allows React hooks to be used for checking motion preferences.
[5]
MotionToggle is a custom component that displays the current motion state and provides a
button to toggle between animated and static modes. It uses track() to reactively update
when preferences change, and updateUserPreferences() to modify the animation speed setting.
[6]
Pass the toggle as the SharePanel component. This places it in the top-right corner of the
editor using tldraw's built-in layout system, avoiding custom positioning CSS.
[7]
On mount, we create three pulse shapes to demonstrate the effect. All shapes respond
simultaneously to the motion preference change.
*/
```
## reduced-motion.css
```css
/* Pulse shape container */
.pulse-shape {
display: flex;
align-items: center;
justify-content: center;
pointer-events: all;
background-color: var(--tl-color-low);
border: 1px solid var(--tl-color-low-border);
border-radius: var(--tl-radius-3);
}
.pulse-shape__content {
text-align: center;
}
.pulse-shape__label {
font-size: 14px;
color: var(--tl-color-text-1);
}
/* Shared pulse indicator styles */
.pulse-indicator,
.pulse-indicator--static {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 auto 16px;
}
/* Animated indicator */
.pulse-indicator {
background-color: var(--tl-color-primary);
animation: pulse 2s ease-in-out infinite;
}
/* Static indicator - no animation */
.pulse-indicator--static {
background-color: var(--tl-color-text-3);
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.7;
}
}
/* Motion toggle */
.motion-toggle {
position: absolute;
top: 100px;
left: 20px;
display: flex;
gap: 8px;
align-items: center;
pointer-events: all;
}
.motion-toggle__label {
font-size: 14px;
}
```
--------
# Custom embeds
Category: Configuration
Keywords: embed, custom
Allow custom embeds.
tldraw comes with support for many types of embeds out of the box, but it also comes with the ability to create custom embeds. In this example we'll remove some of the built in embeds and also create a JSFiddle embed.
## App.tsx
```tsx
import {
CustomEmbedDefinition,
DEFAULT_EMBED_DEFINITIONS,
DefaultEmbedDefinitionType,
Tldraw,
} from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
// [1]
const defaultEmbedTypesToKeep: DefaultEmbedDefinitionType[] = ['tldraw', 'youtube']
const defaultEmbedsToKeep = DEFAULT_EMBED_DEFINITIONS.filter((embed) =>
defaultEmbedTypesToKeep.includes(embed.type)
)
// [2]
const customEmbed: CustomEmbedDefinition = {
type: 'jsfiddle',
title: 'JSFiddle',
hostnames: ['jsfiddle.net'],
minWidth: 300,
minHeight: 300,
width: 720,
height: 500,
doesResize: true,
toEmbedUrl: (url) => {
const urlObj = new URL(url)
const matches = urlObj.pathname.match(/\/([^/]+)\/([^/]+)\/(\d+)\/embedded/)
if (matches) {
return `https://jsfiddle.net/${matches[1]}/${matches[2]}/embedded/`
}
return
},
fromEmbedUrl: (url) => {
const urlObj = new URL(url)
const matches = urlObj.pathname.match(/\/([^/]+)\/([^/]+)\/(\d+)\/embedded/)
if (matches) {
return `https://jsfiddle.net/${matches[1]}/${matches[2]}/`
}
return
},
icon: 'https://jsfiddle.net/img/favicon.png',
}
// [3]
const embeds = [...defaultEmbedsToKeep, customEmbed]
export default function CustomEmbedExample() {
return (
{/* [4] */}
)
}
/**
[1]
tldraw has built-in support for embedding content from several popular apps. In this example, we extract the definitions for handling 'tldraw' and 'youtube' content, and discard the rest.
[2]
We will also add support for embedding JSFiddles. Please note that you have to specify an icon that will be displayed in the `EmbedDialog` component.
[3]
We concatenate the filtered embed definitions with our custom JSFiddle one.
[4]
We now pass the custom embed definitions to the `Tldraw` component.
*/
```
--------
# Custom stroke and font sizes
Category: Configuration
Keywords: stroke, font, size, style
Customize the available stroke and font sizes in tldraw.
Override the `STROKE_SIZES`, and `FONT_SIZES` constants to override the default values.
Note that this will change the stroke and font sizes for ALL instances of tldraw. For example, if you run this example locally, and then open another example, the changes will be present there too until you reload!
## App.tsx
```tsx
import { FONT_SIZES, STROKE_SIZES, Tldraw } from 'tldraw'
// Mutate the built-in stroke sizes
STROKE_SIZES.s = 1
STROKE_SIZES.m = 2
STROKE_SIZES.l = 4
STROKE_SIZES.xl = 8
// Mutate the built-in font sizes
FONT_SIZES.s = 12
FONT_SIZES.m = 16
FONT_SIZES.l = 20
FONT_SIZES.xl = 24
export default function StrokeAndFontSizesExample() {
return (
)
}
```
--------
# Environment detection with tlenv and tlenvReactive
Category: Configuration
Keywords: tlenv, tlenvReactive, environment, platform, browser, pointer, touch, mobile
Detect platform, browser, and input device type.
This example demonstrates tldraw's environment detection APIs for building platform-aware and device-adaptive interfaces:
**Static detection (tlenv):**
- Platform detection (macOS, Windows, iOS, Android)
- Browser detection (Safari, Firefox, Chrome)
- Platform-specific keyboard shortcuts (Cmd vs Ctrl)
**Reactive detection (tlenvReactive with useValue):**
- Pointer type detection (coarse/fine) - can change mid-session on hybrid devices
- Real-time updates when switching between touch and mouse input
The example shows practical usage like adapting button sizes based on pointer type - larger touch targets (48px) for coarse pointers (touch) and smaller targets (32px) for fine pointers (mouse/trackpad).
Use `tlenv` for static properties that don't change during the session, and `tlenvReactive` with the `useValue` hook for properties that can change reactively, especially `isCoarsePointer` on touchscreen laptops.
## App.tsx
```tsx
import { Tldraw, TldrawUiButton, TldrawUiIcon, tlenv, tlenvReactive, useValue } from 'tldraw'
import 'tldraw/tldraw.css'
import './environment-detection.css'
// [1]
function EnvironmentInfo() {
// [2]
const isCoarsePointer = useValue('coarse pointer', () => tlenvReactive.get().isCoarsePointer, [
tlenvReactive,
])
// [3]
const buttonSize = isCoarsePointer ? '48px' : '32px'
return (
Pointer type (reactive): {isCoarsePointer ? 'Touch' : 'Mouse'}
{/* [6] Adaptive button based on pointer type */}
alert(`Button size: ${buttonSize}`)}
>
)
}
export default function RequestEnvironmentDetectionWithTlenvAndTlenvreactiveExample() {
return (
)
}
/*
This example demonstrates environment detection using tlenv and tlenvReactive.
[1] Component that displays environment information
[2] Subscribe to tlenvReactive using useValue hook. Since tlenvReactive is an Atom,
call .get() to access its value. The isCoarsePointer property updates when users
switch between touch and mouse input.
[3] Calculate button size based on pointer type - touch needs larger targets (48px)
while mouse can use smaller buttons (32px)
[4] Display static environment detection with tlenv - platform (isDarwin, isIos, isAndroid)
and browser (isSafari, isFirefox, isChromeForIos). Note: isIos is checked before isDarwin
because iPadOS reports as Mac, making both isDarwin and isIos true on iPads.
[5] Display reactive pointer detection that updates when input method changes
[6] Adaptive button that changes size based on pointer type
[7] Render component in front of canvas using InFrontOfTheCanvas slot
Key differences:
- tlenv: Static object, direct property access (tlenv.isDarwin)
- tlenvReactive: Reactive Atom, requires useValue(() => tlenvReactive.get().property, [tlenvReactive])
*/
```
## environment-detection.css
```css
.environment-info {
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px;
padding: 12px;
}
```
--------
# Deep links
Category: Configuration
Keywords: basic, intro, simple, quick, start
Allow linking to specific parts of a tldraw canvas.
Deep Links are URLs which point to a specific part of a document. We provide a comprehensive set of tools to help you create and manage deep links in your application.
## The `deepLinks` prop
The highest-level API for managing deep links is the `deepLinks` prop on the `` component. This prop is designed for manipulating `window.location` to add a search param which tldraw can use to navigate to a specific part of the document.
e.g. `https://my-app.com/document-name?d=v1234.-234.3.21`
If you set `deepLinks` to `true` e.g. `` the following default behavior will be enabled:
1. When the editor initializes, before the initial render, it will check the current `window.location` for a search param called `d`. If found, it will try to parse the value of this param as a deep link and navigate to that part of the document.
2. 500 milliseconds after every time the editor finishes navigating to a new part of the document, it will update `window.location` to add the latest version of the `d` param.
You can customize this behavior by passing a configuration object as the `deepLinks` prop. e.g.
```tsx
```
For full options see the [`TLDeepLinkOptions`](/reference/editor/reference/editor/TLDeepLinkOptions) API reference.
## Handling deep links manually
We expose the core functionality for managing deep links as a set of methods and utilities. This gives you more control e.g. if you prefer not to use search params in the URL.
### Creating a deep link
You can create an isolated deep link string using the [`createDeepLinkString`](/reference/editor/reference/editor/createDeepLinkString) helper which takes a [`TLDeepLink`](/reference/editor/reference/editor/TLDeepLink) descriptor object.
```tsx
createDeepLinkString({ type: 'page', pageId: 'page:abc123' })
// => 'pabc123'
createDeepLinkString({ type: 'shapes', shapeIds: ['shape:foo', 'shape:bar'] })
// => 'sfoo.bar'
createDeepLinkString({
type: 'viewport',
pageId: 'page:abc123',
bounds: {
x: 0,
y: 0,
w: 1024,
h: 768,
},
})
// => 'v0.0.1024.768.abc123'
```
If you do prefer to put this in a URL as a query param, you can use the [`Editor.createDeepLink`](/reference/editor/reference/editor/Editor#createDeepLink) method.
```tsx
editor.createDeepLink({ to: { type: 'page', pageId: 'page:abc123' } })
// => 'https://my-app.com/document-name?d=pabc123'
```
### Handling a deep link
You can parse a deep link string with [`parseDeepLinkString`](/reference/editor/reference/editor/parseDeepLinkString) which returns a [`TLDeepLink`](/reference/editor/reference/editor/TLDeepLink) descriptor object.
You can then call [`Editor.navigateToDeepLink`](/reference/editor/reference/editor/Editor#navigateToDeepLink) with this descriptor to navigate to the part of the document described by the deep link.
`Editor#navigateToDeepLink` also can take a plain URL if the deep link is encoded as a query param.
```tsx
editor.navigateToDeepLink(parseDeepLinkString('pabc123'))
// or pass in a url
editor.navigateToDeepLink({ url: 'https://my-app.com/document-name?d=pabc123' })
// or call without options to use the current `window.location`
editor.navigateToDeepLink()
```
### Listening for deep link changes
You can listen for deep link changes with the [`Editor.registerDeepLinkListener`](/reference/editor/reference/editor/Editor#registerDeepLinkListener) method, which takes the same options as the `deepLinks` prop.
```tsx
useEffect(() => {
const unlisten = editor.registerDeepLinkListener({
paramName: 'page',
getTarget(editor) {
return { type: 'page', pageId: editor.getCurrentPageId() }
},
onChange(url) {
console.log('the new search params are', url.searchParams)
},
debounceMs: 100,
})
return () => {
unlisten()
}
}, [])
```
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function DeepLinksExample() {
return (
)
}
```
--------
# Frame colors
Category: Configuration
Keywords: frame, color
Use the `showColors` option to display colored fills and headings on frame shapes.
Use the `showColors` option to display colored fills and headings on frame shapes.
Create a frame (keyboard shortcut **F**) and see how it looks with the `showColors` option enabled.
## App.tsx
```tsx
import { FrameShapeUtil, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
const ConfiguredFrameShapeUtil = FrameShapeUtil.configure({ showColors: true })
export default function FrameColorsExample() {
return (
<>
>
)
}
```
--------
# Note resizing
Category: Configuration
Keywords: note, resize, sticky
Make the note shape resizable.
The editor's options include alternative resizing behavior for note shapes. Set `resizeMode` to either `none` for the default behavior or `scale` to allow a user to scale the note.
## App.tsx
```tsx
import { NoteShapeUtil, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
// Configure the note shape util to allow scaling to resize
const shapeUtils = [NoteShapeUtil.configure({ resizeMode: 'scale' })]
export default function ResizeNoteExample() {
return (
<>
{/* pass the configured shape utils to the editor */}
>
)
}
```
--------
# Precise exact arrows
Category: Configuration
Keywords: arrow, configure, isPrecise, isExact
Make arrows adopt "isExact" behavior instead of "isPrecise".
Arrows can be precise, which means that instead of just pointing to the center of a shape, they point to a specific place within the shape. If they're precise, they can also be exact. When they're the exact, the arrow won't stop at the edge of the shape, it'll pass through the shape right the way to the point the arrow is targeting.
By default, arrows are precise when moving slowly, and exact when holding the alt key. This example shows how to customize this behavior, so that a precise arrow is always exact.
## App.tsx
```tsx
import { ArrowShapeUtil, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
const shapeUtils = [
ArrowShapeUtil.configure({
shouldBeExact: (editor, isPrecise) => isPrecise,
}),
]
export default function ArrowsPreciseExactExample() {
return (
)
}
```
--------
# Disable pages
Category: Configuration
Keywords: basic, intro, simple, quick, start, page
Disabling page-related UI for single-page use cases.
You can set the `maxPages` option to `1` to disable the page selector and other UI related to managing pages.
## App.tsx
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
// You can set the `maxPages` option to 1 to disable UI related to managing multiple pages.
export default function DisablePagesExample() {
return (
)
}
```
--------
# Minimal
Category: Configuration
Keywords: select, tool, editor, bare, bones
Use the `` component to render a bare-bones editor with minimal built-in shapes and tools.
This example shows how the `` component can be used to render a bare-bones editor. It uses minimal built-in shapes and tools.
## App.tsx
```tsx
/* eslint-disable import/no-extraneous-dependencies */
import { Editor, TldrawEditor, createShapeId } from '@tldraw/editor'
import 'tldraw/tldraw.css'
import { MiniBoxShapeUtil } from './MiniBoxShape'
import { MiniSelectTool } from './MiniSelectTool'
// There's a guide at the bottom of this page!
// [1]
const myTools = [MiniSelectTool]
const myShapeUtils = [MiniBoxShapeUtil]
// [2]
export default function OnlyEditorExample() {
return (
)
}
/*
This example shows how to use the TldrawEditor component on its own. This is useful if you want to
create your own custom UI, shape and tool interactions.
[1]
We create a custom tool and shape util arrays. These are arrays of classes that extend
the built-in state node and shape util classes. Check out MiniSelectTool.ts and
MiniBoxShapeUtil.tsx to see how they work. Or check out the custom config example for
a more in-depth look at how to create custom tools and shapes.
There is an even simpler implementation of the select tool in MicroSelectTool.tsx, but it
isn't used in this example.
[2]
We pass our custom tools and shape utils to the TldrawEditor component. We also pass in our custom
background component to the background prop and set the initial state to the 'select' tool.
*/
```
## MicroSelectTool.ts
```ts
import { StateNode, TLClickEventInfo, TLPointerEventInfo, createShapeId } from 'tldraw'
// There's a guide at the bottom of this file!
//[1]
export class MicroSelectTool extends StateNode {
static override id = 'select'
//[2]
override onPointerDown(info: TLPointerEventInfo) {
const { editor } = this
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.getCurrentPagePoint())
if (hitShape) {
this.onPointerDown({
...info,
shape: hitShape,
target: 'shape',
})
return
}
editor.selectNone()
break
}
case 'shape': {
editor.select(info.shape.id)
break
}
}
}
//[3]
override onDoubleClick(info: TLClickEventInfo) {
const { editor } = this
if (info.phase !== 'up') return
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.getCurrentPagePoint())
if (hitShape) {
this.onDoubleClick({
...info,
shape: hitShape,
target: 'shape',
})
return
}
const currentPagePoint = editor.inputs.getCurrentPagePoint()
editor.createShapes([
{
id: createShapeId(),
type: 'box',
x: currentPagePoint.x - 50,
y: currentPagePoint.y - 50,
props: {
w: 100,
h: 100,
},
},
])
break
}
case 'shape': {
editor.deleteShapes([info.shape.id])
break
}
}
}
}
/*
This is a very small example of a state node that implements a "select" tool. It
doesn't implement any children states.
The state handles two events: onPointerDown [2] and onDoubleClick [2].
When the user points down on the canvas, it deselects all shapes; and when
they point a shape it selects that shape. When the user double clicks on the
canvas, it creates a new shape; and when they double click on a shape, it
deletes that shape.
[1]
This is where we define our state node by extending the StateNode class. Since
there are no children states We can give it an id and define methods we
want to override to handle events.
[2] onPointerDown
The user clicked on something, let's figure out what it was. We can
access the editor via this.editor, and then use it to check if we hit
a shape. If we did then we call the onPointerDown method again with the
shape as the target, select the shape, and return. If we didn't hit a
shape then we deselect all shapes.
[3] onDoubleClick
The user double clicked on something, let's do the same as above. If we
hit a shape then we call the onDoubleClick method again with the shape as
the target, delete it, and return. If we didn't hit a shape then we create
a new shape at the pointer's position.
*/
```
## MiniBoxShape.tsx
```tsx
import { BaseBoxShapeUtil, HTMLContainer, TLShape } from 'tldraw'
// There's a guide at the bottom of this page!
const BOX_TYPE = 'box'
// [1]
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[BOX_TYPE]: { w: number; h: number; color: string }
}
}
// [2]
export type MiniBoxShape = TLShape
// [3]
export class MiniBoxShapeUtil extends BaseBoxShapeUtil {
//[a]
static override type = BOX_TYPE
//[b]
override getDefaultProps(): MiniBoxShape['props'] {
return { w: 100, h: 100, color: '#efefef' }
}
//[c]
component(shape: MiniBoxShape) {
return (
)
}
//[d]
indicator(shape: MiniBoxShape) {
return
}
}
/*
This is our shape util, in tldraw all shapes extend the shape util class. In this
example we're extending the built-in BaseBoxShapeUtil class. This class provides
the functionality for our shape.
[1]
First, we need to extend TLGlobalShapePropsMap to add our shape's props to the global type system.
This tells TypeScript about the shape's properties. For this shape, we define width (w), height (h),
and color as the shape's properties.
[2]
Define the shape type using TLShape with the shape's type as a type argument.
[3]
The shape util itself.
[a] The type of shape this util is for, this should match the shape type we defined in [2].
[b] The default props for our shape. These will be used when creating a new shape.
[c] The component for our shape. This returns JSX and is what will be rendered on the
canvas. The HtmlContainer component is a div that provides some useful styles.
[d] The indicator for our shape, this also returns JSX. This is what will be rendered
on the canvas when the shape is selected.
*/
```
## MiniSelectTool.ts
```ts
import { StateNode, TLClickEventInfo, TLPointerEventInfo, TLShape, createShapeId } from 'tldraw'
// There's a guide at the bottom of this file!
//[1]
export class MiniSelectTool extends StateNode {
static override id = 'select'
static override children() {
return [IdleState, PointingState, DraggingState]
}
static override initial = 'idle'
}
//[2]
class IdleState extends StateNode {
static override id = 'idle'
//[a]
override onPointerDown(info: TLPointerEventInfo) {
const { editor } = this
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.getCurrentPagePoint())
if (hitShape) {
this.onPointerDown({
...info,
shape: hitShape,
target: 'shape',
})
return
}
editor.selectNone()
break
}
case 'shape': {
if (editor.inputs.getShiftKey()) {
editor.select(...editor.getSelectedShapeIds(), info.shape.id)
} else {
if (!editor.getSelectedShapeIds().includes(info.shape.id)) {
editor.select(info.shape.id)
}
this.parent.transition('pointing', info)
}
break
}
}
}
//[b]
override onDoubleClick(info: TLClickEventInfo) {
const { editor } = this
if (info.phase !== 'up') return
switch (info.target) {
case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.getCurrentPagePoint())
if (hitShape) {
this.onDoubleClick({
...info,
shape: hitShape,
target: 'shape',
})
return
}
const currentPagePoint = editor.inputs.getCurrentPagePoint()
editor.createShapes([
{
id: createShapeId(),
type: 'box',
x: currentPagePoint.x - 50,
y: currentPagePoint.y - 50,
props: {
w: 100,
h: 100,
},
},
])
break
}
case 'shape': {
editor.deleteShapes([info.shape.id])
break
}
}
}
}
//[3]
class PointingState extends StateNode {
static override id = 'pointing'
//[a]
override onPointerUp(info: TLPointerEventInfo) {
this.parent.transition('idle', info)
}
//[b]
override onPointerMove() {
if (this.editor.inputs.getIsDragging()) {
this.parent.transition('dragging', { shapes: [...this.editor.getSelectedShapes()] })
}
}
}
//[4]
class DraggingState extends StateNode {
static override id = 'dragging'
//[a]
private initialDraggingShapes: TLShape[] = []
//[b]
override onEnter(info: { shapes: TLShape[] }) {
this.initialDraggingShapes = info.shapes
}
//[c]
override onPointerUp(info: TLPointerEventInfo) {
this.parent.transition('idle', info)
}
//[d]
override onPointerMove() {
const { initialDraggingShapes } = this
const originPagePoint = this.editor.inputs.getOriginPagePoint()
const currentPagePoint = this.editor.inputs.getCurrentPagePoint()
this.editor.updateShapes(
initialDraggingShapes.map((shape) => {
return {
...shape,
x: shape.x + (currentPagePoint.x - originPagePoint.x),
y: shape.y + (currentPagePoint.y - originPagePoint.y),
}
})
)
}
}
/*
This is where we implement our select tool. In tldraw, tools are part of the
tldraw state chart. Check out the docs for more info:
https://tldraw.dev/docs/editor#State-Chart
Our state node [1] has three children: idle [2], pointing [3], and dragging [4].
Only one child state can be "active" at a time. The parent state's initial active
state is "idle". Certain events received by the child states will cause the parent
state to transition to another child state, making that state active instead.
Note that when `transition()` is called, the parent state will call the new
active state(s)'s `onEnter` method with the second argument passed to the
transition method. This is useful for passing data between states.
[1]
This is where we define our state node by extending the StateNode class. We
give it an id, a list of children states, and its initial active state.
[2]
The idle state is the tool's default state. This is where most of the action is.
We have some handy methods available to help us handle events:
[a] onPointerDown
The user clicked on something, let's figure out what it was. We can
access the editor via this.editor, and then use it to check if we hit
a shape. If we did then we call the onPointerDown method again with the
shape as the target, select the shape and transition to the pointing state.
Otherwise we deselect everything.
[b] onDoubleClick
The user double clicked on something, let's do the same thing as above.
If we hit a shape then we call the onDoubleClick method again with the
shape as the target, and delete the shape. Otherwise we create a new shape.
[3]
The pointing state is something of a transitionary state. Its job is to transition
to the dragging state when the user starts dragging, or go back to the idle state
on pointer up.
[a] onPointerUp
The user let go of the mouse, let's go back to the idle state.
[b] onPointerMove
The user moved the mouse, let's double check they're dragging. If they are
then let's transition to the dragging state and pass it the shapes that
are being dragged.
[4]
The dragging state is where we actually move the shapes around. It's job is to
update the position of the shapes being dragged, and transition back to the idle
state when the user lets go of the mouse.
[a] initialDraggingShapes
We'll use this to keep track of the shapes being dragged when we enter
the state.
[b] onEnter
When we enter the dragging state, we'll save the shapes being dragged.
[c] onPointerUp
The user let go of the mouse, let's go back to the idle state.
[d] onPointerMove
The user moved the mouse, let's update the position of the shapes being
dragged using editor.updateShapes().
*/
```
--------
# Sublibraries
Category: Configuration
Keywords: ui, components, utils
Tldraw is built from several sublibraries - like the editor, default shapes & tools, and UI.
Tldraw is built from several sublibraries - like the editor, default shapes & tools, and UI. For full customization, you can use these sublibraries directly, or replace them with your own.
## App.tsx
```tsx
import { useEffect } from 'react'
import {
ContextMenu,
DEFAULT_SUPPORTED_IMAGE_TYPES,
DEFAULT_SUPPORT_VIDEO_TYPES,
DefaultContextMenuContent,
TldrawEditor,
TldrawHandles,
TldrawOverlays,
TldrawScribble,
TldrawSelectionForeground,
TldrawShapeIndicators,
TldrawUi,
defaultAddFontsFromNode,
defaultBindingUtils,
defaultEditorAssetUrls,
defaultShapeTools,
defaultShapeUtils,
defaultTools,
registerDefaultExternalContentHandlers,
registerDefaultSideEffects,
tipTapDefaultExtensions,
useEditor,
useToasts,
useTranslation,
} from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
// [1]
const defaultComponents = {
Scribble: TldrawScribble,
ShapeIndicators: TldrawShapeIndicators,
CollaboratorScribble: TldrawScribble,
SelectionForeground: TldrawSelectionForeground,
Handles: TldrawHandles,
Overlays: TldrawOverlays,
}
const allDefaultTools = [...defaultTools, ...defaultShapeTools]
const defaultTextOptions = {
tipTapConfig: {
extensions: tipTapDefaultExtensions,
},
addFontsFromNode: defaultAddFontsFromNode,
}
//[2]
export default function ExplodedExample() {
return (
)
}
function InsideEditorAndUiContext() {
const editor = useEditor()
const toasts = useToasts()
const msg = useTranslation()
// [3]
useEffect(() => {
registerDefaultExternalContentHandlers(editor, {
maxImageDimension: 5000,
maxAssetSize: 10 * 1024 * 1024, // 10mb
acceptedImageMimeTypes: DEFAULT_SUPPORTED_IMAGE_TYPES,
acceptedVideoMimeTypes: DEFAULT_SUPPORT_VIDEO_TYPES,
toasts,
msg,
})
const cleanupSideEffects = registerDefaultSideEffects(editor)
return () => {
cleanupSideEffects()
}
}, [editor, msg, toasts])
return (
)
}
/*
The tldraw library is built from many sublibraries. This example shows how to build the tldraw
component from its subcomponents for max customisation. You can edit, omit or add to these
subcomponents to create your app.
[1] Here we've imported some components from the tldraw library which we will later pass to the
TldrawEditor component. These components are not part of the more minimal defaults, so we need to
import them separately. For help creating your own components to pass into the components prop check
out the custom components example.
[2] Here we've passed the default components object to the TldrawEditor component. Along with
default tools and shapeutils, You could input your own custom shapes/tools here. For help creating
your own shapes/tools check out the custom config example.
We also set the initial state to 'select' and render the UI, context menu and canvas components. You
could add your own custom components here, omit these ones, and/or change the initial state of the
application to whatever you want.
[3] Inside of the editor and UI context, we need to set up extra pieces to get the editor working
with our default shapes and tools. We register the default external content handlers, which sets up
handling for things like images and pasted content. We also register the default side effects, which
react to changes to the editor's store.
*/
```
--------
# Controlling the canvas
Category: Editor API
Keywords: api, create shape, update shape, mount, context
Manipulate the contents of the canvas using the editor API.
You can manipulate the contents of the canvas using the editor API. This example creates and updates shapes, selects and rotates them, and zooms the camera.
## App.tsx
```tsx
import {
DefaultColorStyle,
Editor,
TLGeoShape,
TLShapePartial,
Tldraw,
toRichText,
createShapeId,
useEditor,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { useEffect } from 'react'
// There's a guide at the bottom of this file!
//[1]
export default function APIExample() {
const handleMount = (editor: Editor) => {
// Create a shape id
const id = createShapeId('hello')
// Create a shape
editor.createShapes([
{
id,
type: 'geo',
x: 128 + Math.random() * 500,
y: 128 + Math.random() * 500,
props: {
geo: 'rectangle',
w: 120,
h: 100,
dash: 'draw',
color: 'blue',
size: 'm',
},
},
])
// Get the created shape
const shape = editor.getShape(id)!
// Update the shape
editor.updateShape({
id,
type: 'geo',
props: {
h: shape.props.h * 3,
richText: toRichText('hello world!'),
},
})
// Rotate the shape around its center
editor.rotateShapesBy([id], Math.PI / 8)
// Zoom the camera to fit the shape
editor.zoomToFit()
// Select the shape
editor.select(id)
}
return (
)
}
//[2]
const InsideOfEditorContext = () => {
const editor = useEditor()
useEffect(() => {
let i = 0
const interval = setInterval(() => {
const selection = [...editor.getSelectedShapeIds()]
editor.selectAll()
editor.setStyleForSelectedShapes(DefaultColorStyle, i % 2 ? 'blue' : 'light-blue')
editor.setStyleForNextShapes(DefaultColorStyle, i % 2 ? 'blue' : 'light-blue')
editor.setSelectedShapes(selection)
i++
}, 1000)
return () => {
clearInterval(interval)
}
}, [editor])
return null
}
/*
Introduction:
This example shows how to use the tldraw editor instance to make changes
to the canvas. The editor instance is tldraw's "god object". You can use
the app to do just about everything that's possible in tldraw. Internally,
the canvas component and all shapes, tools, and UI components use this instance
to send events, observe changes, and perform actions.
There are two main ways to use the editor:
[1]
The tldraw component shares its editor instance via its onMount callback prop.
When you define a function for the onMount callback, it receives the editor
instance as an argument. You can use this to manipulate the canvas.
[2]
Another (sneakier) way to access the current app is through React context.
The Tldraw component provides the context, so you can add children to
the component and access the app through the useEditor hook. This is cool.
*/
```
--------
# Save and load snapshots
Category: Editor API
Keywords:
Sand and load a snapshot of the editor's contents.
Use `getSnapshot()` and `loadSnapshot()` to save and restore the editor's contents.
## App.tsx
```tsx
import { useCallback, useEffect, useState } from 'react'
import { TLEditorSnapshot, Tldraw, getSnapshot, loadSnapshot, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import _jsonSnapshot from './snapshot.json'
// There's a guide at the bottom of this file!
const jsonSnapshot = _jsonSnapshot as any as TLEditorSnapshot
// [1]
function SnapshotToolbar() {
const editor = useEditor()
const save = useCallback(() => {
// [2]
const { document, session } = getSnapshot(editor.store)
// [3]
localStorage.setItem('snapshot', JSON.stringify({ document, session }))
}, [editor])
const load = useCallback(() => {
const snapshot = localStorage.getItem('snapshot')
if (!snapshot) return
// [4]
loadSnapshot(editor.store, JSON.parse(snapshot))
}, [editor])
const [showCheckMark, setShowCheckMark] = useState(false)
useEffect(() => {
if (showCheckMark) {
const timeout = setTimeout(() => {
setShowCheckMark(false)
}, 1000)
return () => clearTimeout(timeout)
}
return
})
return (
Saved ✅
)
}
export default function SnapshotExample() {
return (
)
}
/*
[1] We'll add a toolbar to the top-right of the editor viewport that allows the user to save and load snapshots.
[2] Call `getSnapshot(editor.store)` to get the current state of the editor
[3] The 'document' state is the set of shapes and pages and images etc.
The 'session' state is the state of the editor like the current page, camera positions, zoom level, etc.
You probably need to store these separately if you're building a multi-user app, so that you can store per-user session state.
For this example we'll just store them together in localStorage.
[4] Call `loadSnapshot()` to load a snapshot into the editor
You can omit the `session` state, or load it later on its own.
e.g.
loadSnapshot(editor.store, { document })
then optionally later
loadSnapshot(editor.store, { session })
[5] You can load an initial snapshot into the editor by passing it to the `snapshot` prop.
*/
```
--------
# Coordinate system transformations
Category: Editor API
Keywords: coordinates, screenToPage, pageToScreen, pageToViewport, getViewportScreenBounds, getViewportPageBounds, viewport, transformation
Convert between screen, page, and viewport coordinate systems.
This example demonstrates how to work with tldraw's three coordinate systems:
- **Screen space**: Pixel coordinates relative to the browser window (from DOM events)
- **Page space**: Coordinates in the infinite canvas space (where shapes live)
- **Viewport space**: Coordinates relative to the editor container (useful for UI positioning)
The key transformation methods demonstrated are:
- `editor.screenToPage(point)` - Convert DOM event coordinates to canvas coordinates
- `editor.pageToScreen(point)` - Position DOM elements relative to shapes
- `editor.pageToViewport(point)` - Get coordinates relative to the editor container
- `editor.getViewportScreenBounds()` - Get visible area bounds in screen coordinates
- `editor.getViewportPageBounds()` - Get visible area bounds in page coordinates
Move your mouse to see coordinates in all three systems. Pan and zoom the canvas to see how screen coordinates stay constant while page coordinates change. Create and select a shape to see a DOM overlay positioned using pageToScreen().
## App.tsx
```tsx
import { useEffect, useState } from 'react'
import { Tldraw, TLEditorComponents, track, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import './coordinate-system.css'
// [1]
const CoordinateDebugPanel = track(() => {
const editor = useEditor()
const [mouseScreen, setMouseScreen] = useState({ x: 0, y: 0 })
const [mousePage, setMousePage] = useState({ x: 0, y: 0 })
const [mouseViewport, setMouseViewport] = useState({ x: 0, y: 0 })
// [2]
useEffect(() => {
const handlePointerMove = (e: PointerEvent) => {
const screenPoint = { x: e.clientX, y: e.clientY }
const pagePoint = editor.screenToPage(screenPoint)
const viewportPoint = editor.pageToViewport(pagePoint)
setMouseScreen(screenPoint)
setMousePage(pagePoint)
setMouseViewport(viewportPoint)
}
const container = editor.getContainer()
container.addEventListener('pointermove', handlePointerMove)
return () => {
container.removeEventListener('pointermove', handlePointerMove)
}
}, [editor])
// [3]
const viewportScreenBounds = editor.getViewportScreenBounds()
const viewportPageBounds = editor.getViewportPageBounds()
return (
)
}
/*
Introduction:
This example demonstrates tldraw's coordinate system transformations. The editor uses three
coordinate systems: screen space (browser pixels), page space (infinite canvas), and viewport space
(relative to the editor container). Understanding these transformations is essential for custom
tools, UI overlays, and interactive features.
[1]
The `CoordinateDebugPanel` displays real-time coordinate information. We use `track` to make it
reactive to editor state changes, ensuring it updates when the camera moves or zooms.
[2]
We track mouse movement to demonstrate coordinate transformations. For each pointer move:
- Screen coordinates come from the DOM event (clientX, clientY)
- Page coordinates are computed using `screenToPage()` - accounting for pan and zoom
- Viewport coordinates are computed using `pageToViewport()` - relative to the editor container
[3]
We also display the viewport bounds in both coordinate systems using `getViewportScreenBounds()`
and `getViewportPageBounds()`. These methods show the visible area of the canvas in screen and
page coordinates respectively. Watch how the screen bounds stay constant while page bounds change
as you pan and zoom the canvas.
[4]
The viewport bounds display shows position (x, y) and size (w×h) for both coordinate systems.
This is useful for visibility checks, culling, and understanding what portion of the infinite
canvas is currently visible.
[5]
The `SelectedShapeOverlay` demonstrates `pageToScreen()` by positioning a DOM element above
the selected shape. This component tracks editor state to update when selection or camera changes.
[6]
We convert the shape's page position to viewport coordinates using `pageToViewport()`. This is the
correct transformation for positioning DOM elements inside `InFrontOfTheCanvas`, which is positioned
relative to the editor container (not the browser window). Using `pageToScreen()` here would cause
the overlay to be displaced when the editor has an offset from the browser origin (e.g., a sidebar).
[7]
We use the `InFrontOfTheCanvas` component slot to render our UI. Components here are positioned
in screen space - they don't scale with zoom but maintain their screen position.
*/
```
## coordinate-system.css
```css
.coordinate-debug-panel {
position: absolute;
top: 64px;
left: 16px;
width: 280px;
background: white;
border: 1px solid #ccc;
padding: 12px;
font-family: system-ui, sans-serif;
font-size: 12px;
z-index: 10000;
user-select: none;
}
.coordinate-debug-panel h3 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
}
.coordinate-debug-panel h4 {
margin: 8px 0 4px 0;
font-size: 11px;
font-weight: 600;
color: #666;
}
.coordinate-section {
margin-bottom: 8px;
}
.coordinate-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
}
.coordinate-label {
color: #666;
}
.coordinate-value {
font-family: monospace;
font-size: 11px;
}
.shape-overlay {
position: absolute;
pointer-events: none;
z-index: 10000;
background: #333;
color: white;
padding: 4px 8px;
font-size: 11px;
font-family: monospace;
white-space: nowrap;
}
```
--------
# Create an arrow
Category: Editor API
Keywords: arrow, between
Create an arrow between two shapes.
This example shows how you can use the Editor API to create an arrow between two shapes.
## App.tsx
```tsx
import { createShapeId, Editor, TLArrowBinding, Tldraw, TLShapeId, Vec } from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
export default function CreateArrowExample() {
return (
<>
{
// Only do this on an empty canvas
if (editor.getCurrentPageShapeIds().size > 0) return
const shapeAId = createShapeId()
const shapeBId = createShapeId()
editor.createShapes([
{
id: shapeAId,
type: 'geo',
x: 100,
y: 100,
},
{
id: shapeBId,
type: 'geo',
x: 400,
y: 400,
},
])
createArrowBetweenShapes(editor, shapeAId, shapeBId)
}}
/>
>
)
}
function createArrowBetweenShapes(
editor: Editor,
startShapeId: TLShapeId,
endShapeId: TLShapeId,
options = {} as {
parentId?: TLShapeId
start?: Partial>
end?: Partial>
}
) {
const { start = {}, end = {}, parentId } = options
// [1]
const {
normalizedAnchor: startNormalizedAnchor = { x: 0.5, y: 0.5 },
isExact: startIsExact = false,
isPrecise: startIsPrecise = false,
} = start
const {
normalizedAnchor: endNormalizedAnchor = { x: 0.5, y: 0.5 },
isExact: endIsExact = false,
isPrecise: endIsPrecise = false,
} = end
const startTerminalNormalizedPosition = Vec.From(startNormalizedAnchor)
const endTerminalNormalizedPosition = Vec.From(endNormalizedAnchor)
const parent = parentId ? editor.getShape(parentId) : undefined
if (parentId && !parent) throw Error(`Parent shape with id ${parentId} not found`)
const startShapePageBounds = editor.getShapePageBounds(startShapeId)
const endShapePageBounds = editor.getShapePageBounds(endShapeId)
const startShapePageRotation = editor.getShapePageTransform(startShapeId).rotation()
const endShapePageRotation = editor.getShapePageTransform(endShapeId).rotation()
if (!startShapePageBounds || !endShapePageBounds) return
const startTerminalPagePosition = Vec.Add(
startShapePageBounds.point,
Vec.MulV(
startShapePageBounds.size,
Vec.Rot(startTerminalNormalizedPosition, startShapePageRotation)
)
)
const endTerminalPagePosition = Vec.Add(
endShapePageBounds.point,
Vec.MulV(
startShapePageBounds.size,
Vec.Rot(endTerminalNormalizedPosition, endShapePageRotation)
)
)
const arrowPointInParentSpace = Vec.Min(startTerminalPagePosition, endTerminalPagePosition)
if (parent) {
arrowPointInParentSpace.setTo(
editor.getShapePageTransform(parent.id)!.applyToPoint(arrowPointInParentSpace)
)
}
const arrowId = createShapeId()
editor.run(() => {
editor.markHistoryStoppingPoint('creating_arrow')
editor.createShape({
id: arrowId,
type: 'arrow',
// [2]
x: arrowPointInParentSpace.x,
y: arrowPointInParentSpace.y,
props: {
// [3]
start: {
x: arrowPointInParentSpace.x - startTerminalPagePosition.x,
y: arrowPointInParentSpace.x - startTerminalPagePosition.x,
},
end: {
x: arrowPointInParentSpace.x - endTerminalPagePosition.x,
y: arrowPointInParentSpace.x - endTerminalPagePosition.x,
},
},
})
editor.createBindings([
{
fromId: arrowId,
toId: startShapeId,
type: 'arrow',
props: {
terminal: 'start',
normalizedAnchor: startNormalizedAnchor,
isExact: startIsExact,
isPrecise: startIsPrecise,
},
},
{
fromId: arrowId,
toId: endShapeId,
type: 'arrow',
props: {
terminal: 'end',
normalizedAnchor: endNormalizedAnchor,
isExact: endIsExact,
isPrecise: endIsPrecise,
},
},
])
})
}
/*
Introduction:
This example shows how to create an arrow between two shapes.
[1]
The normalized anchor is the position inside the shape that the arrow connects to, where 0 0 is the
top left corner and 1 1 is the bottom right. `isPrecise` needs to be enabled for this position to be
used, otherwise it targets the center of the shape.
By default, arrows don't intersect shapes they're connected to, and instead gracefully touch the
outside of the shape's geometry. You can turn this off and make an arrow intersect a shape by
setting `isExact` to true.
[2]
The arrow shape's position is in parent space, which in this case means the page.
[3]
The arrow's start and end positions are "local", which means they're relative to the arrow's
position. Note: You don't need to set the arrow's start and end positions if they're bound to
another shape, as it gets calculated automatically.
*/
```
--------
# Custom clipping shape
Category: Editor API
Keywords: clipping, shape
# Custom clipping shape example
This example demonstrates the extensible clipping system in tldraw, showing how to create custom shapes that can clip their children with any polygon geometry.
## Key implementation details
### ShapeUtil methods
The clipping system uses two methods in the `ShapeUtil` base class:
```typescript
// Returns the clip path polygon in local coordinates
getClipPath(shape: Shape): Vec[] | undefined
// Determines which children should be clipped
shouldClipChild(child: TLShape): boolean
```
### Circle clip shape
The `CircleClipShapeUtil` demonstrates:
- **Custom Geometry**: Uses a polygon approximation of a circle for clipping.
- **Conditional Clipping**: Can be enabled/disabled via shape properties or other state.
- **Selective Clipping**: Text shapes can be selectively excluded from clipping via override.
- **Visual Indicators**: Different appearance based on clipping state.
## Usage
1. Use the **Circle Clip Tool** (circle icon) in the toolbar to create circular clipping shapes
2. Click the **"✂️ Toggle Clipping"** button in the top-left to enable/disable clipping for all circle shapes globally
3. Click the **"📝 Text Clipping Override"** button to toggle whether text shapes should be clipped (when override is ON, text shapes are not clipped regardless of global setting)
4. The example starts with demo content already clipped by a circular shape
## Technical notes
- Clip paths are defined in the shape's local coordinate system
- The Editor automatically transforms them to page space for rendering
- Multiple clipping ancestors are supported (intersected together)
- Performance is optimized through computed caching
- The `shouldClipChild` method can be used to selectively exclude certain shapes from clipping if needed.
## App.tsx
```tsx
import {
createShapeId,
DefaultToolbar,
DefaultToolbarContent,
TLComponents,
Tldraw,
TldrawUiMenuItem,
TLGeoShapeProps,
TLTextShapeProps,
TLUiOverrides,
toRichText,
useEditor,
useIsToolSelected,
useTools,
useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { CircleClipShapeTool } from './CircleClipShapeTool'
import { CircleClipShapeUtil, isClippingEnabled$ } from './CircleClipShapeUtil'
import './CustomClipping.css'
// There's a guide at the bottom of this file!
// [1]
const shapeUtils = [CircleClipShapeUtil]
const tools = [CircleClipShapeTool]
// [2]
const customUiOverrides: TLUiOverrides = {
tools: (editor: any, tools: any) => {
return {
...tools,
'circle-clip': {
id: 'circle-clip',
label: 'Circle Clip',
icon: 'color',
kbd: 'c',
onSelect() {
editor.setCurrentTool('circle-clip')
},
},
}
},
}
// [3]
function ToggleClippingButton() {
const editor = useEditor()
const clippingEnabled = useValue('isClippingEnabled', () => isClippingEnabled$.get(), [editor])
return (
)
}
/*
Introduction:
This example demonstrates the extensible clipping system in tldraw, showing how to create custom shapes
that can clip their children with any polygon geometry. The clipping system uses two key methods:
`getClipPath` to define the clip boundary and `shouldClipChild` to control which children get clipped.
[1]
We define arrays to hold our custom shape util and tool. It's important to do this outside of any React
component so that these arrays don't get redefined on every render.
[2]
Here we define UI overrides to add our custom circle clip tool to the toolbar. The `tools` override
allows us to add new tools with custom icons, labels, and keyboard shortcuts.
[3]
The ToggleClippingButton component demonstrates how to create global state management for clipping.
It uses the `isClippingEnabled$` atom to toggle clipping on/off for all circle clip shapes.
[4]
The CustomToolbar component shows how to integrate custom tools into the main toolbar. We use
`useIsToolSelected` to highlight the active tool and `TldrawUiMenuItem` to render the tool button.
[5]
We define custom components to override the default toolbar and add our toggle button in front of
the canvas. The `components` prop allows us to customize various parts of the tldraw UI.
[6]
This is where we render the Tldraw component with our custom shape utils, tools, components, and
overrides. The onMount callback sets up the initial demo content.
For more details on the clipping implementation, see CircleClipShapeUtil.tsx and CircleClipShapeTool.tsx.
*/
```
## CircleClipShapeTool.tsx
```tsx
import { StateNode, TLEventHandlers } from 'tldraw'
export class CircleClipShapeTool extends StateNode {
static override id = 'circle-clip'
override onEnter(): void {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
override onPointerDown(info: Parameters[0]) {
if (info.target === 'canvas') {
const originPagePoint = this.editor.inputs.getOriginPagePoint()
this.editor.createShape({
type: 'circle-clip',
x: originPagePoint.x - 100,
y: originPagePoint.y - 100,
props: {
w: 200,
h: 200,
},
})
}
}
}
```
## CircleClipShapeUtil.tsx
```tsx
import {
BaseBoxShapeUtil,
Circle2d,
Geometry2d,
PI2,
RecordProps,
SVGContainer,
T,
TLResizeInfo,
TLShape,
Vec,
atom,
clamp,
resizeBox,
toDomPrecision,
} from 'tldraw'
const CIRCLE_CLIP_TYPE = 'circle-clip'
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[CIRCLE_CLIP_TYPE]: {
w: number
h: number
}
}
}
export type CircleClipShape = TLShape
export const isClippingEnabled$ = atom('isClippingEnabled', true)
// The stroke width used when rendering the circle
const STROKE_WIDTH = 3
export class CircleClipShapeUtil extends BaseBoxShapeUtil {
static override type = CIRCLE_CLIP_TYPE
static override props: RecordProps = {
w: T.number,
h: T.number,
}
override canBind() {
return false
}
override canReceiveNewChildrenOfType(shape: TLShape) {
return !shape.isLocked
}
override providesBackgroundForChildren(): boolean {
return true
}
override getDefaultProps(): CircleClipShape['props'] {
return {
w: 200,
h: 200,
}
}
override getGeometry(shape: CircleClipShape): Geometry2d {
const radius = Math.min(shape.props.w, shape.props.h) / 2
return new Circle2d({
radius,
x: shape.props.w / 2 - radius,
y: shape.props.h / 2 - radius,
isFilled: true,
})
}
override getClipPath(shape: CircleClipShape): Vec[] | undefined {
// Generate a polygon approximation of the circle.
// We inset the clip path by half the stroke width so that children are
// clipped to the inner edge of the stroke, not the center line.
const centerX = shape.props.w / 2
const centerY = shape.props.h / 2
const outerRadius = Math.min(shape.props.w, shape.props.h) / 2
const clipRadius = outerRadius - STROKE_WIDTH / 2
const segments = clamp(Math.round((PI2 * clipRadius) / 8), 3, 360) // More segments = smoother circle
const points: Vec[] = []
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * Math.PI * 2
const x = centerX + Math.cos(angle) * clipRadius
const y = centerY + Math.sin(angle) * clipRadius
points.push(new Vec(x, y))
}
return points
}
override shouldClipChild(_child: TLShape): boolean {
// For now, clip all children - we removed the onlyClipText feature for simplicity
return isClippingEnabled$.get()
}
override component(shape: CircleClipShape) {
const radius = Math.min(shape.props.w, shape.props.h) / 2
const centerX = shape.props.w / 2
const centerY = shape.props.h / 2
const clippingEnabled = isClippingEnabled$.get()
return (
{/* Visual indicator */}
{clippingEnabled ? '✂️' : '○'}
)
}
override indicator(shape: CircleClipShape) {
const radius = Math.min(shape.props.w, shape.props.h) / 2
const centerX = shape.props.w / 2
const centerY = shape.props.h / 2
return (
)
}
override onResize(shape: CircleClipShape, info: TLResizeInfo) {
return resizeBox(shape, info)
}
}
```
## CustomClipping.css
```css
.CustomClipping-toggleButton {
position: absolute;
top: 64px;
left: 12px;
pointer-events: all;
z-index: 1000;
}
.CustomClipping-button {
padding: 8px 16px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.CustomClipping-button--enabled {
background-color: #ea4335;
}
.CustomClipping-button--disabled {
background-color: #34a853;
}
```
--------
# Zoom to bounds
Category: Editor API
Keywords: zoom, camera, bounds, inset
Zoom the camera to specific bounds using the editor's `zoomToBounds` function.
This example shows how to use the `zoomToBounds` method to programmatically zoom the camera to specific areas of the canvas. The example creates two boxes at different positions and provides buttons to zoom to each one. Note that the bounds the camera ultimately moves to may be different than the bounds you pass into the function. This is because the values of the bounds are massaged to maintain your viewport's aspect ratio. Also, if an inset is not specified, a default one will be set, adding inset padding to the bounds.
## App.tsx
```tsx
import { Box, createShapeId, Tldraw, TldrawUiButton, useEditor } from 'tldraw'
import './zoom-to-bounds.css'
export default function ZoomToBoundsExample() {
const zoomBox1 = new Box(50, 100, 900, 720)
const zoomBox2 = new Box(1000, 500, 500, 400)
return (
editor.zoomToBounds(zoomBox1, { inset: 72 })}
>
Zoom to violet box
editor.zoomToBounds(zoomBox2, { inset: 72, animation: { duration: 200 } })
}
>
Zoom to blue box
editor.zoomToBounds(Box.Common([zoomBox1, zoomBox2]), {
inset: 200,
animation: { duration: 200 },
})
}
>
Zoom to both boxes
)
},
}}
/>
)
}
```
## zoom-to-bounds.css
```css
.control-panel {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 8px;
}
```
--------
# Create a video shape
Category: Editor API
Keywords: asset, video
Create a video shape using a local asset.
This example shows how to create a `TLVideoAsset` for a video file that's hosted locally and add a `VideoShape` referencing that asset to the canvas.
## App.tsx
```tsx
import { useCallback } from 'react'
import { AssetRecordType, Editor, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function LocalVideosExample() {
const handleMount = useCallback((editor: Editor) => {
const assetId = AssetRecordType.createId()
const videoWidth = 640
const videoHeight = 360
editor.createAssets([
{
id: assetId,
type: 'video',
typeName: 'asset',
props: {
name: 'fluid.mp4',
src: '/fluid.mp4',
w: videoWidth,
h: videoHeight,
mimeType: 'video/mp4',
isAnimated: true,
},
meta: {},
},
])
editor.createShape({
type: 'video',
x: (window.innerWidth - videoWidth) / 2,
y: (window.innerHeight - videoHeight) / 2,
props: {
assetId,
w: videoWidth,
h: videoHeight,
},
})
}, [])
return (
)
}
```
--------
# Create an image shape
Category: Editor API
Keywords: asset, record, create asset
Create an image shape using a local asset.
This example shows how to use local images in the built-in `ImageShape` shape. You must first create an asset that holds the source of the image, then create a shape that references the asset.
## App.tsx
```tsx
import { useCallback } from 'react'
import { AssetRecordType, Editor, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
export default function LocalImagesExample() {
// [1]
const handleMount = useCallback((editor: Editor) => {
//[2]
const assetId = AssetRecordType.createId()
const imageWidth = 1200
const imageHeight = 675
//[2]
editor.createAssets([
{
id: assetId,
type: 'image',
typeName: 'asset',
props: {
name: 'tldraw.png',
src: '/tldraw.png', // You could also use a base64 encoded string here
w: imageWidth,
h: imageHeight,
mimeType: 'image/png',
isAnimated: false,
},
meta: {},
},
])
//[3]
editor.createShape({
type: 'image',
// Let's center the image in the editor
x: (window.innerWidth - imageWidth) / 2,
y: (window.innerHeight - imageHeight) / 2,
props: {
assetId,
w: imageWidth,
h: imageHeight,
},
})
}, [])
return (
)
}
/*
This is an example of how you can add a locally hosted image to the editor.
We need to first create an asset that holds the source image [2], and then
create the Image shape itself [3].
Because this is a Next.js app, we can use the `public` folder to store the
image locally, your framework may have a different way of serving static
assets.
If you want to allow users to upload the images please take a look at the
hosted images example.
[1]
We'll access the editor instance via the `onMount` callback. Check out the API
example for another way to do this.
[2]
Assets are records that store data about shared assets like images, videos, etc.
Each image has an associated asset record, so we'll create that first. We need an
`assetId` so that we can later associate it with the image.
[3]
We create the image shape and pass in the `assetId` that we created earlier. This
will link our image shape to the asset record. Notice that we create the shape with
the same dimensions as the image, later on the user may resize the image, but we
don't want to resize our asset, this is one of the reasons why it's important to
keep assets and shapes separate.
*/
```
--------
# Dynamic tools with setTool and removeTool
Category: Editor API
Keywords: setTool, removeTool, dynamic, tools, state chart, toolbar
Dynamically add and remove tools from the editor and toolbar after initialization.
The `setTool` and `removeTool` methods allow you to add and remove tools from the editor's state chart on demand, after the editor has been initialized. This example shows how to dynamically add a tool that appears in the toolbar when added and disappears when removed. This is useful when you need to conditionally enable or disable tools based on user permissions, feature flags, or other runtime conditions.
## App.tsx
```tsx
import { useMemo, useState } from 'react'
import {
DefaultToolbar,
DefaultToolbarContent,
Editor,
StateNode,
TLComponents,
TLTextShape,
TLUiAssetUrlOverrides,
TLUiOverrides,
Tldraw,
TldrawUiButton,
TldrawUiMenuItem,
toRichText,
useIsToolSelected,
useTools,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './dynamic-tools.css'
// There's a guide at the bottom of this file!
const OFFSET = 12
// [1]
class HeartTool extends StateNode {
static override id = 'heart'
override onEnter() {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
override onPointerDown() {
const currentPagePoint = this.editor.inputs.getCurrentPagePoint()
this.editor.createShape({
type: 'text',
x: currentPagePoint.x - OFFSET,
y: currentPagePoint.y - OFFSET,
props: { richText: toRichText('❤️') },
})
}
}
// [2]
const uiOverrides: TLUiOverrides = {
tools(editor, tools) {
// Create a tool item in the ui's context.
tools.heart = {
id: 'heart',
icon: 'heart-icon',
label: 'Heart',
kbd: 'r',
onSelect: () => {
editor.setCurrentTool('heart')
},
}
return tools
},
}
// [3]
export const customAssetUrls: TLUiAssetUrlOverrides = {
icons: {
'heart-icon': '/heart-icon.svg',
},
}
// [4]
export default function DynamicToolsExample() {
const [editor, setEditor] = useState(null)
const [isHeartToolEnabled, setIsHeartToolEnabled] = useState(false)
// [5]
const components: TLComponents = useMemo(
() => ({
Toolbar: (props) => {
const tools = useTools()
const isHeartSelected = useIsToolSelected(tools['heart'])
return (
{isHeartToolEnabled && (
)}
)
},
InFrontOfTheCanvas: () => {
const toggleHeartTool = () => {
if (!editor) return
if (isHeartToolEnabled) {
// [6]
editor.removeTool(HeartTool)
// Switch to select tool if we're currently on the heart tool
if (editor.getCurrentToolId() === 'heart') {
editor.setCurrentTool('select')
}
} else {
// [7]
editor.setTool(HeartTool)
}
setIsHeartToolEnabled(!isHeartToolEnabled)
}
return (
setEditor(editor)}
// pass in our custom asset urls
assetUrls={customAssetUrls}
/>
)
}
/*
Introduction:
This example demonstrates how to use the `setTool` and `removeTool` methods to dynamically
add and remove tools from the editor's state chart after initialization. When the tool is
added, it also appears in the toolbar dynamically. This is useful when you need to
conditionally enable or disable tools based on runtime conditions like user permissions,
feature flags, or application state.
[1]
We define a simple HeartTool that extends StateNode. It creates a heart emoji sticker
when you click on the canvas. This tool is NOT passed to the Tldraw component initially -
it will be added dynamically using setTool.
[2]
We define UI overrides to add the heart tool to the UI context. This makes it available
for the toolbar component to reference, even if the tool hasn't been added to the state
chart yet.
[3]
We override the Toolbar component to conditionally show the heart tool. The tool only
appears in the toolbar when it exists in the state chart (isHeartToolEnabled is true).
This creates a nice dynamic behavior where the toolbar updates when you add/remove the tool.
[4]
We pass the overrides and components to the Tldraw component. The toggle button will
appear on top of the canvas, and clicking it will add/remove the heart tool dynamically.
When added, the tool appears in the toolbar and can be used immediately.
[5]
We define the components object. We override the Toolbar component to conditionally show the heart tool. The tool only
appears in the toolbar when it exists in the state chart (isHeartToolEnabled is true).
This creates a nice dynamic behavior where the toolbar updates when you add/remove the tool.
[6]
We remove the heart tool from the state chart if it exists, and adds it back if it doesn't.
[7]
We add the heart tool to the state chart if it doesn't exist.
*/
```
## dynamic-tools.css
```css
.toggle-button-container {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: auto;
}
```
--------
# Focus the editor
Category: Editor API
Keywords: instance, state, keyboard shortcuts
Manually manage the editor's focus to enable or disable keyboard shortcuts.
In this example, we drive the editor's focus in order to turn on and off keyboard shortcuts.
The editor's focus is different from—but usually corresponds to—the browser's concept of "focus", which is related to the document's [active element](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement).
Unfortunately, the browser's focus cannot be relied on to determine whether the editor's keyboard shortcuts should work. While its possible to detect whether the document's active element is a descendant of the Tldraw component's own element, it's not 100% reliable. For example, iframes are not considered descendants of their parents, and many menus are portalled into different parts of the document tree.
For these reasons, the responsibility falls to you, dear developer, to manage focus for your Tldraw editor, especially in cases where there are more than one editor on the same page.
## App.tsx
```tsx
import { useRef } from 'react'
import { Editor, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import './editor-focus.css'
export default function EditorFocusExample() {
const editorRef = useRef(null)
return (
You should be able to type in this text input without worrying about triggering editor
shortcuts even when the editor is focused.
When the editor is “focused”, its keyboard shortcuts will work and scrolling over the editor
will scroll the canvas. When it is not focused, the keyboard shortcuts will not work and
scrolling over the editor will not move the canvas.
{
editorRef.current = editor
}}
/>
)
}
```
## editor-focus.css
```css
.tl-container__focused {
outline: 1px solid var(--tl-color-primary);
}
```
--------
# Lock camera zoom
Category: Editor API
Keywords: camera, lock, zoom
Lock the camera at a specific zoom level.
Need to lock the camera at its current zoom level? You can use the camera controls API to keep the zoom level from changing.
In this example, press Shift+K to lock the camera at its current zoom level. Press Shift+L to unlock the camera and allow it to zoom again.
## App.tsx
```tsx
import { Tldraw, TLUiOverrides } from 'tldraw'
import 'tldraw/tldraw.css'
const DEFAULT_CAMERA_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8]
const overrides: TLUiOverrides = {
actions(editor, actions) {
actions.lockCameraZoom = {
id: 'lock-camera-zoom',
kbd: 'shift+k',
onSelect() {
const isCameraZoomLockedAlready = editor.getCameraOptions().zoomSteps.length === 1
editor.setCameraOptions({
zoomSteps: isCameraZoomLockedAlready ? DEFAULT_CAMERA_STEPS : [editor.getZoomLevel()],
})
},
}
return actions
},
}
export default function BasicExample() {
return (
)
}
```
--------
# Search text on the canvas
Category: Editor API
Keywords: zoom, pan, camera bounds, search, text
Search through all the text on the canvas.
A simple example of how to use the API to search for text in shapes and display results in the `HelperButtons` part of the user interface. Press Ctrl+F or Cmd+F to open the search bar.
## App.tsx
```tsx
import { TLComponents, TLUiOverrides, Tldraw, atom, track } from 'tldraw'
import 'tldraw/tldraw.css'
import { TextSearchPanel } from './TextSearchPanel'
import './text-search.css'
export const showSearch = atom('showSearch', false)
const components: TLComponents = {
HelperButtons: TextSearchPanel,
}
const overrides: TLUiOverrides = {
actions(_editor, actions) {
return {
...actions,
'text-search': {
id: 'text-search',
label: 'Search',
kbd: 'cmd+f,ctrl+f',
onSelect() {
if (!showSearch.get()) {
showSearch.set(true)
}
},
},
}
},
}
const TextSearchExample = track(() => {
return (
)
}
/*
[1]
Create a component with buttons to trigger different animations. Use the `useEditor` hook to
access the editor instance.
[2]
`animateShape()` animates a single shape to new property values. Pass a partial shape with the
target values and animation options. The `easing` property accepts functions from the `EASINGS`
object. In this example, we move the shape to a new position with cubic easing.
[3]
Animate rotation by specifying a target rotation value in radians. Here we rotate the shape
360 degrees (2π radians) with smooth easing.
[4]
Opacity can be animated between 0 and 1. This creates a fade effect. We toggle between
low and high opacity values using quadratic easing.
[5]
Multiple properties can be animated simultaneously. This example combines position, rotation,
and opacity changes in a single animation.
[6]
`animateShapes()` animates multiple shapes at once. Build an array of shape partials and
pass them all together. All shapes will animate with the same duration and easing function.
*/
```
## shape-animation.css
```css
.animation-controls {
display: flex;
flex-wrap: wrap;
margin: 8px;
}
```
--------
# Align and distribute shapes
Category: Editor API
Keywords: align, distribute, layout, position, arrange
Align and distribute shapes using the editor's built-in methods.
This example demonstrates how to use the `alignShapes` and `distributeShapes` methods to programmatically arrange shapes on the canvas. The example creates 5 shapes at different positions and provides buttons to align them (left, center-horizontal, right, top, center-vertical, bottom) or distribute them (horizontal, vertical). Select multiple shapes and click the buttons to see the alignment and distribution in action. Note that align operations require at least 2 shapes, while distribute operations require at least 3 shapes.
## App.tsx
```tsx
import { useRef } from 'react'
import { createShapeId, Tldraw, TldrawUiButton, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import './align-and-distribute-shapes.css'
// [1]
const ALIGN_OPERATIONS = [
{ operation: 'left', label: 'Align left' },
{ operation: 'center-horizontal', label: 'Align center H' },
{ operation: 'right', label: 'Align right' },
{ operation: 'top', label: 'Align top' },
{ operation: 'center-vertical', label: 'Align center V' },
{ operation: 'bottom', label: 'Align bottom' },
] as const
function ControlPanel({
originalPositions,
}: {
originalPositions: React.RefObject