Align and distribute shapes

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.

import { useRef } from 'react'
import { createShapeId, Tldraw, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import './request-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 AlignButtons() {
	const editor = useEditor()

	return (
		<div className="align-distribute-controls">
			{ALIGN_OPERATIONS.map(({ operation, label }) => (
				<button
					key={operation}
					className="align-distribute-button"
					onClick={() => {
						// [2]
						const selectedIds = editor.getSelectedShapeIds()
						if (selectedIds.length > 1) {
							editor.alignShapes(selectedIds, operation)
						}
					}}
				>
					{label}
				</button>
			))}
		</div>
	)
}

// [3]
const DISTRIBUTE_OPERATIONS = [
	{ operation: 'horizontal', label: 'Distribute horizontal' },
	{ operation: 'vertical', label: 'Distribute vertical' },
] as const

function DistributeButtons() {
	const editor = useEditor()

	return (
		<div className="align-distribute-controls distribute-row">
			{DISTRIBUTE_OPERATIONS.map(({ operation, label }) => (
				<button
					key={operation}
					className="align-distribute-button"
					onClick={() => {
						// [4]
						const selectedIds = editor.getSelectedShapeIds()
						if (selectedIds.length > 2) {
							editor.distributeShapes(selectedIds, operation)
						}
					}}
				>
					{label}
				</button>
			))}
		</div>
	)
}

function ResetButton({
	originalPositions,
}: {
	originalPositions: React.RefObject<Map<string, { x: number; y: number }>>
}) {
	const editor = useEditor()

	return (
		<div className="align-distribute-controls distribute-row">
			<button
				className="align-distribute-button"
				onClick={() => {
					const shapes = editor.getCurrentPageShapes()
					editor.run(() => {
						for (const shape of shapes) {
							const originalPos = originalPositions.current?.get(shape.id)
							if (originalPos) {
								editor.updateShape({
									...shape,
									x: originalPos.x,
									y: originalPos.y,
								})
							}
						}
					})
				}}
			>
				Reset positions
			</button>
		</div>
	)
}

export default function RequestAlignAndDistributeShapesExample() {
	const originalPositions = useRef(new Map<string, { x: number; y: number }>())

	return (
		<div className="tldraw__editor">
			<Tldraw
				onMount={(editor) => {
					const shapes = [
						{
							id: createShapeId(),
							type: 'geo' as const,
							x: 100,
							y: 100,
							props: {
								w: 100,
								h: 100,
								color: 'blue' as const,
							},
						},
						{
							id: createShapeId(),
							type: 'geo' as const,
							x: 300,
							y: 200,
							props: {
								w: 120,
								h: 80,
								color: 'red' as const,
							},
						},
						{
							id: createShapeId(),
							type: 'geo' as const,
							x: 500,
							y: 150,
							props: {
								w: 80,
								h: 120,
								color: 'green' as const,
							},
						},
						{
							id: createShapeId(),
							type: 'geo' as const,
							x: 150,
							y: 400,
							props: {
								w: 100,
								h: 100,
								color: 'violet' as const,
							},
						},
						{
							id: createShapeId(),
							type: 'geo' as const,
							x: 400,
							y: 450,
							props: {
								w: 90,
								h: 90,
								color: 'orange' as const,
							},
						},
					]

					for (const shape of shapes) {
						originalPositions.current.set(shape.id, { x: shape.x, y: shape.y })
					}

					editor.createShapes(shapes)
					editor.selectAll()
				}}
				components={{
					TopPanel: () => (
						<>
							<AlignButtons />
							<DistributeButtons />
							<ResetButton originalPositions={originalPositions} />
						</>
					),
				}}
			/>
		</div>
	)
}

/*
[1]
Define an array of all align operations with their labels. This makes it easy to render buttons for each operation without repetition. The available operations are: left, center-horizontal, right (horizontal alignment), and top, center-vertical, bottom (vertical alignment).

[2]
The alignShapes method requires at least 2 shapes to be selected. It aligns the selected shapes based on the specified operation. The shapes parameter can be either shape IDs or shape objects.

[3]
Define an array of distribute operations. Distribution evenly spaces shapes along the specified axis: horizontal or vertical.

[4]
The distributeShapes method requires at least 3 shapes to be selected. It distributes shapes evenly along the horizontal or vertical axis, maintaining equal spacing between them.
*/
Is this page helpful?
Prev
Search text on the canvas
Next
Lasso select tool