Custom 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:

// 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.
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 (
		<div className="CustomClipping-toggleButton">
			<button
				className={`CustomClipping-button ${
					clippingEnabled ? 'CustomClipping-button--enabled' : 'CustomClipping-button--disabled'
				}`}
				onClick={() => {
					isClippingEnabled$.update((prev) => !prev)
				}}
				onPointerDown={editor.markEventAsHandled}
				onPointerUp={editor.markEventAsHandled}
			>
				{clippingEnabled ? '✂️ Disable Clipping' : '○ Enable Clipping'}
			</button>
		</div>
	)
}

// [4]
function CustomToolbar() {
	const tools = useTools()
	const isCircleClipSelected = useIsToolSelected(tools['circle-clip'])

	return (
		<DefaultToolbar>
			<TldrawUiMenuItem {...tools['circle-clip']} isSelected={isCircleClipSelected} />
			<DefaultToolbarContent />
		</DefaultToolbar>
	)
}

// [5]
const components: TLComponents = {
	Toolbar: CustomToolbar,
	InFrontOfTheCanvas: ToggleClippingButton,
}

// [6]
export default function CustomClippingExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw
				shapeUtils={shapeUtils}
				tools={tools}
				components={components}
				overrides={customUiOverrides}
				onMount={(editor) => {
					editor.setCurrentTool('select')

					const clipShapeId = createShapeId()
					editor.createShape({
						id: clipShapeId,
						type: 'circle-clip',
						x: 200,
						y: 200,
						props: {
							w: 300,
							h: 300,
						},
					})

					editor.createShape({
						type: 'text',
						x: 0,
						y: 100,
						parentId: clipShapeId,
						props: {
							size: 'l',
							textAlign: 'middle',
							richText: toRichText('This text is clipped to the circle! ✂️'),
						} satisfies Partial<TLTextShapeProps>,
					})

					editor.createShape({
						type: 'geo',
						x: 100,
						y: 290,
						parentId: clipShapeId,
						props: {
							geo: 'rectangle',
							w: 200,
							h: 100,
							fill: 'solid',
							color: 'blue',
							richText: toRichText('Oops you found me!'),
						} satisfies Partial<TLGeoShapeProps>,
					})

					editor.zoomToFit()
				}}
			/>
		</div>
	)
}

/*
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.

*/
Is this page helpful?
Prev
Create an arrow
Next
Zoom to bounds