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.
You can create custom style properties for your shapes using StyleProp. Styles are special properties that persist across shape creation and can be edited via the style panel.
Creating a custom style
ShapeWithCustomStylesExample.tsx
import {
BaseBoxShapeUtil,
DefaultStylePanel,
DefaultStylePanelContent,
HTMLContainer,
StyleProp,
T,
Tldraw,
TLShape,
useEditor,
useRelevantStyles,
} from 'tldraw'
import 'tldraw/tldraw.css'
const MY_SHAPE_WITH_CUSTOM_STYLES_TYPE = 'myshapewithcustomstyles'
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[MY_SHAPE_WITH_CUSTOM_STYLES_TYPE]: {
w: number
h: number
rating: MyRatingStyle
}
}
}
// [1] Define the custom style
const myRatingStyle = StyleProp.defineEnum('example:rating', {
defaultValue: 1,
values: [1, 2, 3, 4, 5],
})
// [2] Extract the type
type MyRatingStyle = T.TypeOf<typeof myRatingStyle>
type IMyShape = TLShape<typeof MY_SHAPE_WITH_CUSTOM_STYLES_TYPE>
class MyShapeUtil extends BaseBoxShapeUtil<IMyShape> {
static override type = MY_SHAPE_WITH_CUSTOM_STYLES_TYPE
// [3] Include the style in props
static override props = {
w: T.number,
h: T.number,
rating: myRatingStyle,
}
getDefaultProps(): IMyShape['props'] {
return {
w: 300,
h: 300,
rating: 4,
}
}
component(shape: IMyShape) {
// [4] Use the style in rendering
const stars = ['☆', '☆', '☆', '☆', '☆']
for (let i = 0; i < shape.props.rating; i++) {
stars[i] = '★'
}
return (
<HTMLContainer
id={shape.id}
style={{ backgroundColor: 'var(--tl-color-low-border)', overflow: 'hidden' }}
>
{stars}
</HTMLContainer>
)
}
indicator(shape: IMyShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
// [5] Create custom style panel
function CustomStylePanel() {
const editor = useEditor()
const styles = useRelevantStyles()
if (!styles) return null
const rating = styles.get(myRatingStyle)
return (
<DefaultStylePanel>
<DefaultStylePanelContent />
{rating !== undefined && (
<div>
<select
style={{ width: '100%', padding: 4 }}
value={rating.type === 'mixed' ? '' : rating.value}
onChange={(e) => {
editor.markHistoryStoppingPoint()
const value = myRatingStyle.validate(+e.currentTarget.value)
editor.setStyleForSelectedShapes(myRatingStyle, value)
}}
>
{rating.type === 'mixed' ? <option value="">Mixed</option> : null}
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
</select>
</div>
)}
</DefaultStylePanel>
)
}
export default function ShapeWithCustomStylesExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapeUtils={[MyShapeUtil]}
components={{
StylePanel: CustomStylePanel,
}}
onMount={(editor) => {
editor.createShape({ type: 'myshapewithcustomstyles', x: 100, y: 100 })
editor.selectAll()
editor.createShape({
type: 'myshapewithcustomstyles',
x: 450,
y: 250,
props: { rating: 5 },
})
}}
/>
</div>
)
}
How styles work
Styles are special properties that:
- Persist across shape creation (the editor remembers the last value)
- Can be edited via the style panel for multiple selected shapes
- Automatically handle “mixed” states when multiple shapes have different values
Defining styles
Enum style
const sizeStyle = StyleProp.defineEnum('my-app:size', {
defaultValue: 'medium',
values: ['small', 'medium', 'large'],
})
Number style
const opacityStyle = StyleProp.define('my-app:opacity', {
defaultValue: 1,
type: T.number,
})
Color style
const customColorStyle = StyleProp.defineEnum('my-app:color', {
defaultValue: 'blue',
values: ['red', 'blue', 'green', 'yellow'],
})
Using styles in shape utils
1. Add to props
static override props = {
w: T.number,
h: T.number,
rating: myRatingStyle,
}
2. Set default value
getDefaultProps(): IMyShape['props'] {
return {
w: 200,
h: 200,
rating: 4, // Will be overwritten by editor's current style value
}
}
The default value you set in getDefaultProps will be overwritten by the editor’s current value for that style, which is either the default value or the most recent value the user set.
3. Use in rendering
component(shape: IMyShape) {
const rating = shape.props.rating
// Use rating to render the shape
return <div>{rating}</div>
}
Custom style panel
Use useRelevantStyles to get the styles of selected shapes:
import { useEditor, useRelevantStyles, DefaultStylePanel } from 'tldraw'
function CustomStylePanel() {
const editor = useEditor()
const styles = useRelevantStyles()
if (!styles) return null
const myStyle = styles.get(myCustomStyle)
return (
<DefaultStylePanel>
<DefaultStylePanelContent />
{myStyle !== undefined && (
<select
value={myStyle.type === 'mixed' ? '' : myStyle.value}
onChange={(e) => {
editor.markHistoryStoppingPoint()
const value = myCustomStyle.validate(e.currentTarget.value)
editor.setStyleForSelectedShapes(myCustomStyle, value)
}}
>
{myStyle.type === 'mixed' ? <option value="">Mixed</option> : null}
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
)}
</DefaultStylePanel>
)
}
Setting styles programmatically
// Set style for selected shapes
editor.setStyleForSelectedShapes(myRatingStyle, 5)
// Set style for next shapes to be created
editor.setStyleForNextShapes(myRatingStyle, 5)
// Get current style value
const currentValue = editor.getStyleForNextShape(myRatingStyle)
Handling mixed values
When multiple shapes with different style values are selected, useRelevantStyles returns a “mixed” type:
const rating = styles.get(myRatingStyle)
if (rating.type === 'mixed') {
// Multiple different values
return <span>Mixed</span>
} else {
// Single value
return <span>{rating.value}</span>
}
Style naming convention
Prefix your style IDs with your app name to avoid conflicts:
const myStyle = StyleProp.defineEnum('my-app:style-name', {
defaultValue: 'default',
values: ['default', 'alternative'],
})