Files
upstream-tldraw/apps/docs/content/sdk-features/themes.mdx
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

430 lines
16 KiB
Plaintext

---
title: Themes
created_at: 3/3/2026
updated_at: 3/7/2026
keywords:
- theme
- themes
- color
- dark mode
- light mode
- TLTheme
- TLThemeDefaultColors
- TLThemeColors
- ThemeManager
- getCurrentTheme
- getColorMode
- updateTheme
- useColorMode
status: published
date: 3/7/2026
---
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:
| Variant | Purpose |
| -------------------- | ---------------------------------------------- |
| `solid` | Full-opacity color for strokes and solid fills |
| `semi` | Muted color for the semi fill style |
| `pattern` | Color used in pattern/hatch fills |
| `fill` | Explicit fill color (usually same as solid) |
| `linedFill` | Slightly lighter fill for lined patterns |
| `frameHeadingStroke` | Stroke color for frame headings |
| `frameHeadingFill` | Fill color for frame headings |
| `frameStroke` | Stroke color for frame borders |
| `frameFill` | Fill color for frame backgrounds |
| `frameText` | Text color inside frames |
| `noteFill` | Fill color for note shapes |
| `noteText` | Text color inside note shapes |
| `highlightSrgb` | Highlighter color in sRGB color space |
| `highlightP3` | Highlighter color in Display P3 color space |
The palette also includes thirteen UI colors ([TLThemeUiColorKeys](?)) that every theme must define:
| Property | Purpose |
| ---------------------------------- | ------------------------------------------------------------------------------------------ |
| `text` | Default text color |
| `background` | Canvas background color used in SVG exports |
| `negativeSpace` | Areas that "cut through" to the background, e.g. frame heading knockouts and text outlines |
| `solid` | Default solid surface color |
| `cursor` | Cursor color |
| `noteBorder` | Note shape borders |
| `selectionStroke`, `selectionFill` | Selection box |
| `selectedContrast` | Indicators drawn on top of selected shapes |
| `brushStroke`, `brushFill` | Selection-brush rectangle |
| `snap` | Snap guides |
| `laser` | Laser 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](/sdk-features/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](/examples/ui/multiple-themes).
```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](/examples/ui/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
| Method | Description |
| ----------------------------- | ------------------------------------------------- |
| [Editor#getCurrentTheme](?) | Get the current theme definition |
| [Editor#getCurrentThemeId](?) | Get the id of the current theme |
| [Editor#setCurrentTheme](?) | Set the current theme by id |
| [Editor#getThemes](?) | Get all registered themes |
| [Editor#getTheme](?) | Get a theme by id |
| [Editor#updateTheme](?) | Register or update a theme (keyed by `id`) |
| [Editor#updateThemes](?) | Replace all themes or update via callback |
| [Editor#getColorMode](?) | Get the active color mode (`'light'` or `'dark'`) |
| [Editor#setColorMode](?) | Set the color mode (`'light'` or `'dark'`) |
These methods delegate to an internal [ThemeManager](?).
## Related examples
- [Custom theme](/examples/ui/custom-theme) - Add a custom color, and adjust theme values with sliders.
- [Changing default colors](/examples/ui/changing-default-colors) - Customize the default theme's color palette.
- [Multiple themes](/examples/ui/multiple-themes) - Register several themes and switch between them.
- [Display options](/examples/configuration/display-options) - Override how built-in shapes render using `getCustomDisplayValues`.