Documentation Index Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
Tldraw provides flexible rendering options for shapes through the component() and indicator() methods. Shapes can use SVG for vector graphics, HTML for text and interactive elements, or a mix of both.
Component rendering
The component() method returns a React element that renders the shape’s visual appearance:
component ( shape : MyShape ) {
return (
< SVGContainer >
< rect
width = {shape.props. w }
height = {shape.props. h }
fill = "blue"
stroke = "black"
strokeWidth = { 2 }
/>
</ SVGContainer >
)
}
Container types
Tldraw provides two container components for shape rendering:
SVGContainer
For SVG content that needs proper positioning and transformation:
import { SVGContainer } from 'tldraw'
component ( shape : MyShape ) {
return (
< SVGContainer >
< circle cx = { 50 } cy = { 50 } r = { 40 } fill = "red" />
< path d = "M 10 10 L 90 90" stroke = "black" />
</ SVGContainer >
)
}
Optional ID for the SVG container
Additional CSS styles (use for min-width/min-height)
HTMLContainer
For HTML content like text, forms, or interactive elements:
import { HTMLContainer } from 'tldraw'
component ( shape : MyShape ) {
return (
< HTMLContainer
style = {{
width : shape . props . w ,
height : shape . props . h ,
overflow : 'hidden' ,
}}
>
< div className = "my-shape-content" >
< h1 > Hello World </ h1 >
< p > HTML content here </ p >
</ div >
</ HTMLContainer >
)
}
Both containers automatically handle positioning, rotation, and z-index. You don’t need to apply transforms manually.
Combining SVG and HTML
Shapes often combine both for maximum flexibility:
component ( shape : TLGeoShape ) {
const theme = useDefaultColorTheme ()
return (
<>
< SVGContainer >
< GeoShapeBody shape = { shape } />
</ SVGContainer >
< HTMLContainer
style = {{
width : shape . props . w ,
height : shape . props . h ,
overflow : 'hidden' ,
}}
>
< RichTextLabel
shapeId = {shape. id }
font = {shape.props. font }
fontSize = {LABEL_FONT_SIZES [shape.props.size]}
richText={shape.props.richText}
labelColor={getColorValue(theme, shape.props.labelColor, 'solid')}
/>
</HTMLContainer>
</>
)
}
Indicators
Indicators show when a shape is selected or hovered. They should be simple, non-interactive SVG elements:
SVG indicators (legacy)
indicator ( shape : MyShape ) {
return (
< rect
width = {shape.props. w }
height = {shape.props. h }
fill = "none"
stroke = "var(--color-selected)"
strokeWidth = { 1 }
/>
)
}
Canvas indicators (recommended)
For better performance, use canvas-based indicators:
override useLegacyIndicator () {
return false // Use canvas rendering
}
override getIndicatorPath ( shape : MyShape ): Path2D | undefined {
const path = new Path2D ()
path . rect ( 0 , 0 , shape . props . w , shape . props . h )
return path
}
Canvas indicators are 2-3x faster than SVG indicators for complex shapes.
Complex indicators with clipping
For shapes that need clipping (like arrows with labels):
override getIndicatorPath ( shape : TLArrowShape ): TLIndicatorPath {
const bodyPath = new Path2D ()
// ... build arrow body path
const labelGeometry = this . editor . getShapeGeometry ( shape ). children [ 1 ]
if ( ! labelGeometry ) return bodyPath
// Create clip path
const clipPath = new Path2D ()
// Outer rectangle (what to keep)
clipPath . rect (
bounds . minX - 100 ,
bounds . minY - 100 ,
bounds . width + 200 ,
bounds . height + 200
)
// Label cutout (what to remove)
const lb = labelGeometry . bounds
clipPath . roundRect ( lb . x , lb . y , lb . w , lb . h , 3.5 )
return {
path: bodyPath ,
clipPath: clipPath ,
additionalPaths: [ arrowheadPath ],
}
}
When using clipPath, create the outer rectangle clockwise and cutouts counter-clockwise for proper winding order.
Theming and colors
Using color themes
import { useDefaultColorTheme , getColorValue } from 'tldraw'
component ( shape : MyShape ) {
const theme = useDefaultColorTheme ()
const fillColor = getColorValue ( theme , shape . props . color , 'solid' )
const strokeColor = getColorValue ( theme , shape . props . color , 'outline' )
return (
< SVGContainer >
< rect
width = {shape.props. w }
height = {shape.props. h }
fill = { fillColor }
stroke = { strokeColor }
/>
</ SVGContainer >
)
}
Color variants
Full opacity color for fills and strokes const color = getColorValue ( theme , 'blue' , 'solid' )
Semi-transparent color for highlights const color = getColorValue ( theme , 'blue' , 'semi' )
Pattern fill reference for SVG fills const fill = getColorValue ( theme , 'blue' , 'pattern' )
// Returns: "url(#tldraw-pattern-blue)"
Fill patterns
Tldraw provides built-in fill patterns:
import { ShapeFill , getFillDefForCanvas } from 'tldraw'
component ( shape : MyShape ) {
const theme = useDefaultColorTheme ()
return (
< SVGContainer >
{ shape . props . fill !== ' none ' && (
< ShapeFill
theme = { theme }
d = { pathData }
color = {shape.props. color }
fill = {shape.props. fill }
scale = {shape.props. scale }
/>
)}
< path
d = { pathData }
fill = "none"
stroke = { getColorValue ( theme , shape.props.color, 'solid' )}
strokeWidth = { 2 }
/>
</ SVGContainer >
)
}
getCanvasSvgDefs (): TLShapeUtilCanvasSvgDef [] {
return [ getFillDefForCanvas ()]
}
Register canvas SVG defs with getCanvasSvgDefs() to ensure patterns are available in the SVG context.
Dash patterns
Handle different stroke styles:
const DASH_PATTERNS = {
draw: 'freehand' , // Hand-drawn appearance
solid: 'none' , // Solid line
dashed: '8 8' , // Dashed
dotted: '2 6' , // Dotted
}
component ( shape : MyShape ) {
const strokeDasharray = DASH_PATTERNS [ shape . props . dash ]
return (
< SVGContainer >
< path
d = { pathData }
stroke = "black"
strokeWidth = { 2 }
strokeDasharray = { strokeDasharray }
strokeLinecap = "round"
strokeLinejoin = "round"
/>
</ SVGContainer >
)
}
Font rendering
Registering fonts
Ensure fonts are loaded before rendering:
import { getFontsFromRichText } from 'tldraw'
override getFontFaces ( shape : MyShape ) {
if ( isEmptyRichText ( shape . props . richText )) {
return EMPTY_ARRAY
}
return getFontsFromRichText ( this . editor , shape . props . richText , {
family: `tldraw_ ${ shape . props . font } ` ,
weight: 'normal' ,
style: 'normal' ,
})
}
Rendering text
Use the RichTextLabel component for consistent text rendering:
import { RichTextLabel , FONT_SIZES , TEXT_PROPS } from 'tldraw'
component ( shape : TLTextShape ) {
const theme = useDefaultColorTheme ()
return (
< RichTextLabel
shapeId = {shape. id }
type = "text"
font = {shape.props. font }
fontSize = {FONT_SIZES [shape.props.size]}
lineHeight={TEXT_PROPS.lineHeight}
align={shape.props.textAlign}
verticalAlign="middle"
richText={shape.props.richText}
labelColor={getColorValue(theme, shape.props.color, 'solid')}
isSelected={isSelected}
wrap
showTextOutline={true}
/>
)
}
SVG export
Provide a clean SVG representation for exports:
toSvg ( shape : MyShape , ctx : SvgExportContext ): ReactElement | null {
// Register required definitions
ctx . addExportDef ( getFillDefForExport ( shape . props . fill ))
const theme = getDefaultColorTheme ( ctx )
return (
< g >
< rect
width = {shape.props. w }
height = {shape.props. h }
fill = { getColorValue ( theme , shape.props.color, 'solid' )}
stroke = "black"
strokeWidth = { 2 }
/>
< RichTextSVG
fontSize = {FONT_SIZES [shape.props.size]}
font={shape.props.font}
richText={shape.props.richText}
labelColor={getColorValue(theme, shape.props.color, 'solid')}
bounds={textBounds}
/>
</g>
)
}
Export definitions
Register reusable SVG elements:
getCanvasSvgDefs (): TLShapeUtilCanvasSvgDef [] {
return [
getFillDefForCanvas (),
{
key: 'my-gradient' ,
component: MyGradientDef ,
},
{
key: 'my-marker' ,
component: MyMarkerDef ,
},
]
}
function MyGradientDef () {
return (
< linearGradient id = "my-gradient" >
< stop offset = "0%" stopColor = "red" />
< stop offset = "100%" stopColor = "blue" />
</ linearGradient >
)
}
Zoom-based rendering
Simplify rendering at low zoom levels:
import { useEfficientZoomThreshold } from 'tldraw'
component ( shape : MyShape ) {
const isForceSolid = useEfficientZoomThreshold ( 0.25 / shape . props . scale )
return (
< SVGContainer >
< path
d = { pathData }
stroke = "black"
strokeDasharray = {isForceSolid ? 'none' : '8 8' }
/>
</ SVGContainer >
)
}
Culling
Control whether shapes can be culled when offscreen:
override canCull ( shape : MyShape ): boolean {
return true // Allow culling (default)
}
Culled shapes have display: none applied but remain in the DOM.
Conditional rendering
Only render expensive elements when needed:
component ( shape : MyShape ) {
const isEditing = this . editor . getEditingShapeId () === shape . id
const isEmpty = isEmptyRichText ( shape . props . richText )
const showLabel = isEditing || ! isEmpty
return (
<>
< SVGContainer >
< rect width = {shape.props. w } height = {shape.props. h } />
</ SVGContainer >
{ showLabel && (
< HTMLContainer >
< RichTextLabel { ... labelProps } />
</ HTMLContainer >
)}
</>
)
}
Memoization
Use React hooks to avoid unnecessary recalculations:
import { useMemo } from 'react'
component ( shape : MyShape ) {
const pathData = useMemo (() => {
return buildComplexPath ( shape . props )
}, [ shape . props . w , shape . props . h , shape . props . points ])
return (
< SVGContainer >
< path d = { pathData } />
</ SVGContainer >
)
}
Background rendering
For shapes that need to render content behind other shapes:
override providesBackgroundForChildren ( shape : MyShape ): boolean {
return true // This shape provides a background layer
}
backgroundComponent ( shape : MyShape ) {
return (
< SVGContainer >
< rect
width = {shape.props. w }
height = {shape.props. h }
fill = "white"
opacity = { 0.8 }
/>
</ SVGContainer >
)
}
Clipping child shapes
Define a clip path for child shapes:
getClipPath ( shape : MyFrameShape ): Vec [] | undefined {
const strokeWidth = 2
const inset = strokeWidth / 2
return [
new Vec ( inset , inset ),
new Vec ( shape . props . w - inset , inset ),
new Vec ( shape . props . w - inset , shape . props . h - inset ),
new Vec ( inset , shape . props . h - inset ),
]
}
shouldClipChild ( child : TLShape ): boolean {
// Only clip certain child types
return child . type !== 'text'
}
The clip path should define the inner boundary. Inset by half the stroke width to clip to the inside edge.
Selection bounds customization
override hideSelectionBoundsBg ( shape : MyShape ): boolean {
return false // Show background of selection bounds
}
override hideSelectionBoundsFg ( shape : MyShape ): boolean {
return false // Show foreground of selection bounds
}
override expandSelectionOutlinePx ( shape : MyShape ): number | Box {
return 5 // Expand outline by 5px
}
Next steps
Shape interactions Handle user interactions and events
ShapeUtil API Complete ShapeUtil method reference