Back to Tldraw

Themes

apps/docs/content/sdk-features/themes.mdx

5.4.015.2 KB
Original Source

Themes control the color palette used to render shapes in tldraw. A theme definition bundles color palettes for both light and dark modes alongside shared properties like font size and stroke width. The editor automatically selects the right color mode based on the user's preference.

tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export default function App() {
	return (
		<Tldraw
			onMount={(editor) => {
				// Read the current theme and resolve colors for the active mode
				const theme = editor.getCurrentTheme()
				const colors = theme.colors[editor.getColorMode()]
				console.log(colors.red.solid) // '#e03131'
			}}
		/>
	)
}

Theme structure

A theme definition is a TLTheme object with an id, color palettes (TLThemeColors) for both light and dark modes, font definitions, and shared fontSize, lineHeight, and strokeWidth values. DEFAULT_THEME is the built-in theme:

typescript
import { DEFAULT_THEME, TLTheme } from 'tldraw'

const myTheme: TLTheme = {
	id: 'custom',
	fontSize: 16,
	lineHeight: 1.35,
	strokeWidth: 2,
	fonts: DEFAULT_THEME.fonts,
	colors: {
		light: {
			text: '#000000',
			background: '#f8f8f8',
			negativeSpace: '#f8f8f8',
			solid: '#fcfcfc',
			cursor: '#000000',
			noteBorder: '#e8e8e8',
			// ... other UI colors (selection, brush, snap, laser)
			black: {
				solid: '#1d1d1d',
				semi: '#e8e8e8',
				pattern: '#494949',
				fill: '#1d1d1d',
				linedFill: '#e8e8e8',
				frameHeadingStroke: '#1d1d1d',
				frameHeadingFill: '#f5f5f5',
				frameStroke: '#e2e2e2',
				frameFill: '#fcfcfc',
				frameText: '#1d1d1d',
				noteFill: '#fddd00',
				noteText: '#000000',
				highlightSrgb: '#fddd00',
				highlightP3: 'color(display-p3 0.972 0.8705 0.05)',
			},
			blue: {
				solid: '#4263eb',
				// ... other variants
			},
			// ... other colors
		},
		dark: {
			// ... dark mode equivalents
		},
	},
}

Each TLTheme has a unique id used as the key in the theme registry, and always contains both light and dark palettes under colors. To get the palette for the current color mode, index into theme.colors with the result of Editor#getColorMode.

Each color in the palette is a TLDefaultColor with variants for different contexts:

VariantPurpose
solidFull-opacity color for strokes and solid fills
semiMuted color for the semi fill style
patternColor used in pattern/hatch fills
fillExplicit fill color (usually same as solid)
linedFillSlightly lighter fill for lined patterns
frameHeadingStrokeStroke color for frame headings
frameHeadingFillFill color for frame headings
frameStrokeStroke color for frame borders
frameFillFill color for frame backgrounds
frameTextText color inside frames
noteFillFill color for note shapes
noteTextText color inside note shapes
highlightSrgbHighlighter color in sRGB color space
highlightP3Highlighter color in Display P3 color space

The palette also includes thirteen UI colors (TLThemeUiColorKeys) that every theme must define:

PropertyPurpose
textDefault text color
backgroundCanvas background color used in SVG exports
negativeSpaceAreas that "cut through" to the background, e.g. frame heading knockouts and text outlines
solidDefault solid surface color
cursorCursor color
noteBorderNote shape borders
selectionStroke, selectionFillSelection box
selectedContrastIndicators drawn on top of selected shapes
brushStroke, brushFillSelection-brush rectangle
snapSnap guides
laserLaser pointer

In the default theme, background and negativeSpace have the same value, but custom themes can set them independently.

The background property is used for SVG exports and the fill of selection handles, not for the canvas itself. The visible canvas background comes from the --tl-color-background CSS variable. If you customize background in your theme, set the CSS variable to match, otherwise exports use a different background than the canvas.

In addition to colors, each theme has a fontSize (default 16), lineHeight (default 1.35), and strokeWidth (default 2). All default shape font sizes are derived by multiplying the base fontSize by a per-shape-type multiplier, and stroke widths work the same way with strokeWidth, so changing these base values scales text and strokes proportionally. Because these are shared across light and dark modes, you only set them once per theme definition.

Reading the current theme

Use Editor#getCurrentTheme to get the resolved theme. This is reactive: components that read it will re-render when the theme changes.

tsx
function MyComponent() {
	const editor = useEditor()
	const theme = editor.getCurrentTheme()
	const colors = theme.colors[editor.getColorMode()]

	return <div style={{ color: colors.blue.solid }}>Blue text</div>
}

Inside a shape util, access the theme through this.editor:

typescript
class MyShapeUtil extends ShapeUtil<MyShape> {
	component(shape: MyShape) {
		const theme = this.editor.getCurrentTheme()
		// ...
	}
}

Use getColorValue to resolve a color style name to its actual color value:

typescript
import { getColorValue } from 'tldraw'

const theme = editor.getCurrentTheme()
const colors = theme.colors[editor.getColorMode()]
const color = getColorValue(colors, 'red', 'solid') // '#e03131'

Color mode

The editor selects 'light' or 'dark' colors based on its colorScheme setting. Set the initial color scheme via the colorScheme prop:

tsx
<Tldraw colorScheme="dark" />

The colorScheme prop accepts 'light' (default), 'dark', or 'system' (follows the OS preference).

Use Editor#getColorMode to read the resolved mode at runtime:

typescript
const colorMode = editor.getColorMode() // 'light' or 'dark'

Users can also override the color scheme via their preferences:

typescript
editor.user.updateUserPreferences({ colorScheme: 'dark' })

When set, the user preference takes priority over the colorScheme prop. See User preferences for more.

The useColorMode hook provides a reactive color mode for use in React components:

tsx
function MyComponent() {
	const colorMode = useColorMode() // reactive, re-renders on change
	return <div>Current mode: {colorMode}</div>
}

Customizing colors

Use Editor#updateTheme to override specific colors in a theme. Get the theme, modify it, and pass it back:

typescript
const theme = editor.getTheme('default')!
editor.updateTheme({
	...theme,
	colors: {
		...theme.colors,
		light: { ...theme.colors.light, black: { ...theme.colors.light.black, solid: 'navy' } },
	},
})

You can also override other properties:

typescript
const theme = editor.getTheme('default')!
editor.updateTheme({ ...theme, fontSize: 18, strokeWidth: 3 })

You can also pass theme definitions as the themes prop on the Tldraw or TldrawEditor component, typed as Partial<TLThemes>. The prop is reactive: when it changes, the editor's themes update automatically. Use the initialTheme prop or Editor#setCurrentTheme to pick which registered theme is active; see the multiple themes example.

tsx
import { DEFAULT_THEME, TLThemes } from 'tldraw'

const themes: Partial<TLThemes> = {
	default: {
		id: 'default',
		fontSize: 16,
		lineHeight: 1.35,
		strokeWidth: 2,
		fonts: DEFAULT_THEME.fonts,
		colors: {
			light: { /* ... */ },
			dark: { /* ... */ },
		},
	},
}

<Tldraw themes={themes} />

Adding custom colors

You can add new colors beyond the built-in palette. Extend the TLThemeDefaultColors interface with module augmentation and include the color in your theme definitions:

typescript
import { TLDefaultColor } from 'tldraw'

declare module '@tldraw/tlschema' {
	interface TLThemeDefaultColors {
		pink: TLDefaultColor
	}
}

The augmentation is type-only; your theme definitions supply the runtime values:

tsx
import { DEFAULT_THEME, TLThemes } from 'tldraw'

const themes: Partial<TLThemes> = {
	default: {
		...DEFAULT_THEME,
		colors: {
			light: {
				...DEFAULT_THEME.colors.light,
				pink: {
					solid: '#e91e8c',
					semi: '#fce4f2',
					pattern: '#f06baf',
					fill: '#e91e8c',
					linedFill: '#fce4f2',
					frameHeadingStroke: '#e91e8c',
					frameHeadingFill: '#fce4f2',
					frameStroke: '#e91e8c',
					frameFill: '#fce4f2',
					frameText: '#e91e8c',
					noteFill: '#fce4f2',
					noteText: '#e91e8c',
					highlightSrgb: '#e91e8c',
					highlightP3: '#e91e8c',
				},
			},
			dark: {
				...DEFAULT_THEME.colors.dark,
				pink: {
					solid: '#f06baf',
					semi: '#3d1a2e',
					// ... other variants
				},
			},
		},
	},
}

<Tldraw themes={themes} />

tldraw validates shape properties (including colors) when data enters the store, for example when loading from IndexedDB or syncing from a server. Passing themes tells tldraw about your custom colors before data is loaded, so they pass validation. If you create your own store, pass the same themes to createTLStore so the colors are registered before the store loads data.

Every theme should include an entry for every custom color in both light and dark palettes. If a shape uses a color that doesn't exist in the active theme, it won't render correctly.

See the Custom theme example for a complete demo.

Removing colors

To remove built-in palette colors, augment the TLRemovedDefaultThemeColors interface. Any key you add is omitted from TLThemeColors, so TypeScript no longer expects it in theme definitions:

typescript
declare module '@tldraw/tlschema' {
	interface TLRemovedDefaultThemeColors {
		'light-violet': true
		'light-blue': true
		'light-green': true
		'light-red': true
	}
}

Then omit those colors when building your theme object:

typescript
const {
	'light-violet': _,
	'light-blue': __,
	'light-green': ___,
	'light-red': ____,
	...kept
} = DEFAULT_THEME.colors.light

Colors that are absent from every registered theme are removed from the color style and won't appear in the style panel. The UI colors listed in TLThemeUiColorKeys cannot be removed.

Using themes in shape utils

For built-in shapes, override how theme colors are applied using getCustomDisplayValues on a configured shape util. The theme and colorMode parameters provide the theme definition and active color mode for the current context (which may differ from the editor's active theme during SVG export):

typescript
import { GeoShapeUtil, type GeoShapeUtilDisplayValues } from 'tldraw'

const CustomGeoShapeUtil = GeoShapeUtil.configure({
	getCustomDisplayValues(_editor, shape, theme, colorMode): Partial<GeoShapeUtilDisplayValues> {
		if (shape.isLocked) {
			return { fillColor: theme.colors[colorMode].red.solid }
		}

		return {}
	},
})

Frame, note, and highlight shapes read their colors from the theme's color palette. To customize them, override the relevant color variants in your theme definition:

typescript
import { DEFAULT_THEME, TLTheme } from 'tldraw'

const myTheme: TLTheme = {
	...DEFAULT_THEME,
	id: 'custom',
	colors: {
		light: {
			...DEFAULT_THEME.colors.light,
			noteBorder: '#A5D6A7',
			black: {
				...DEFAULT_THEME.colors.light.black,
				noteFill: '#E8F5E9', // green-tinted notes
				noteText: '#1B5E20',
			},
			blue: {
				...DEFAULT_THEME.colors.light.blue,
				noteFill: '#E3F2FD',
				noteText: '#0D47A1',
			},
			// ... other colors
		},
		dark: DEFAULT_THEME.colors.dark,
	},
}

The same approach works for frame colors (frameFill, frameStroke, frameHeadingFill, frameHeadingStroke, frameText) and highlight colors (highlightSrgb, highlightP3).

For custom shapes, call this.editor.getCurrentTheme() or useEditor().getCurrentTheme() directly in your component and indicator methods:

tsx
class MyShapeUtil extends ShapeUtil<MyShape> {
	component(shape: MyShape) {
		const theme = this.editor.getCurrentTheme()
		const colors = theme.colors[this.editor.getColorMode()]
		const color = getColorValue(colors, shape.props.color, 'solid')

		return (
			<HTMLContainer>
				<div style={{ color }}>Hello</div>
			</HTMLContainer>
		)
	}
}

Editor API reference

MethodDescription
Editor#getCurrentThemeGet the current theme definition
Editor#getCurrentThemeIdGet the id of the current theme
Editor#setCurrentThemeSet the current theme by id
Editor#getThemesGet all registered themes
Editor#getThemeGet a theme by id
Editor#updateThemeRegister or update a theme (keyed by id)
Editor#updateThemesReplace all themes or update via callback
Editor#getColorModeGet the active color mode ('light' or 'dark')
Editor#setColorModeSet the color mode ('light' or 'dark')

These methods delegate to an internal ThemeManager.