apps/docs/content/sdk-features/user-preferences.mdx
User preferences store per-user settings that persist across sessions and synchronize across browser tabs. Access them through editor.user:
import { Tldraw, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
function PreferencesPanel() {
const editor = useEditor()
// Read preferences
const isDark = editor.user.getIsDarkMode()
const animationSpeed = editor.user.getAnimationSpeed()
const locale = editor.user.getLocale()
// Update preferences
const toggleDarkMode = () => {
editor.user.updateUserPreferences({
colorScheme: isDark ? 'light' : 'dark',
})
}
return <button onClick={toggleDarkMode}>Toggle theme</button>
}
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw components={{ TopPanel: PreferencesPanel }} />
</div>
)
}
Preferences fall into three categories: visual settings (color scheme, animation speed), interaction settings (snap mode, edge scroll speed), and identity properties (user name, color, locale). The system stores data in localStorage and uses the BroadcastChannel API to sync changes across tabs in real time.
The UserPreferencesManager exposes each preference as a computed value. These are reactive: when you read them, your component automatically re-renders when the value changes.
// Individual preferences
const isDark = editor.user.getIsDarkMode()
const speed = editor.user.getAnimationSpeed()
const locale = editor.user.getLocale()
const userName = editor.user.getName()
const userColor = editor.user.getColor()
const isSnapMode = editor.user.getIsSnapMode()
// All preferences as an object
const allPrefs = editor.user.getUserPreferences()
Use updateUserPreferences() to change one or more preferences at once:
editor.user.updateUserPreferences({
colorScheme: 'dark',
animationSpeed: 0.5,
isSnapMode: true,
})
Changes apply immediately, save to localStorage, and broadcast to other tabs.
| Preference | Type | Default | Description |
|---|---|---|---|
colorScheme | 'light' | 'dark' | 'system' | 'light' | Theme mode |
animationSpeed | number | 1 (or 0 if reduced motion) | Multiplier for animation durations |
enhancedA11yMode | boolean | false | Additional UI labels and visual aids |
When colorScheme is 'system', the editor tracks the operating system's dark mode preference through a media query listener.
| Preference | Type | Default | Description |
|---|---|---|---|
isSnapMode | boolean | false | Snap shapes to other shapes and guides |
isWrapMode | boolean | false | Enable text wrapping in text shapes |
isDynamicSizeMode | boolean | false | Live shape updates during resize |
isPasteAtCursorMode | boolean | false | Paste at cursor instead of original location |
edgeScrollSpeed | number | 1 | Speed multiplier for edge scrolling during drag |
areKeyboardShortcutsEnabled | boolean | true | Enable or disable keyboard shortcuts |
inputMode | 'trackpad' | 'mouse' | null | null | Optimize behavior for input device |
| Preference | Type | Default | Description |
|---|---|---|---|
id | string | Auto-generated | Unique user identifier |
name | string | '' | Display name shown to collaborators |
color | string | Random from palette | User color for cursor and selections |
locale | string | Browser locale | Language code (e.g., 'en', 'fr') |
The user color is randomly chosen from 12 visually distinct colors designed for collaboration.
The getIsDarkMode() method resolves the color scheme to a boolean. When colorScheme is 'system', it tracks the operating system's preference through a media query listener:
const isDark = editor.user.getIsDarkMode()
// true if colorScheme is 'dark', or 'system' with OS in dark mode
You can also use the inferDarkMode prop on the Tldraw component to automatically infer the initial theme from the user's system preference:
<Tldraw inferDarkMode />
Preferences persist to localStorage under the key TLDRAW_USER_DATA_v3. Each save includes a version number, and the system runs migrations when loading older data to keep preferences compatible across tldraw releases.
The system uses the BroadcastChannel API to sync preference changes across browser tabs. When you change a preference in one tab, all other tabs update automatically. Each tab has a unique origin ID to avoid processing its own broadcasts.
The animationSpeed default respects the prefers-reduced-motion media query. Users with reduced motion enabled get animationSpeed: 0 by default, disabling animations without manual configuration.
Preferences are validated using userTypeValidator from @tldraw/editor. Invalid data falls back to fresh preferences rather than causing errors.
colorScheme.