Actions
Actions are named operations that users trigger from menus, keyboard shortcuts, or custom UI. Each action bundles an identifier, display metadata (label, icon, keyboard shortcut), and a handler function. Actions let you define operations like "undo", "group", or "export as PNG" once and invoke them from multiple places with consistent behavior.
import { Tldraw, TLUiOverrides } from 'tldraw'
import 'tldraw/tldraw.css'
const overrides: TLUiOverrides = {
actions(editor, actions, helpers) {
// Add a custom action
actions['show-selection-count'] = {
id: 'show-selection-count',
label: 'action.show-selection-count',
kbd: 'shift+c',
onSelect(source) {
const count = editor.getSelectedShapeIds().length
helpers.addToast({ title: `${count} shapes selected` })
},
}
return actions
},
}
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw overrides={overrides} />
</div>
)
}The tldraw package includes nearly 100 default actions covering editing, arrangement, export, zoom, and preferences. You can override any of these or add your own through the overrides prop, typed as TLUiOverrides.
How actions work
Actions live in a React context inside the tldraw UI. When the UI mounts, it registers all default actions, applies any overrides you've provided, and makes them available through the useActions hook. Menus and toolbars look up actions by ID and render them with their labels, icons, and keyboard shortcuts.
Each action has an onSelect handler that receives a source parameter indicating where it was triggered:
const actions = useActions()
const duplicateAction = actions['duplicate']
// Trigger programmatically
duplicateAction.onSelect('toolbar')Keyboard shortcuts are bound automatically. The useKeyboardShortcuts hook parses each action's kbd property and registers hotkey handlers. When a shortcut fires, it calls the action's onSelect with 'kbd' as the source.
Action structure
The TLUiActionItem interface defines what an action contains:
| Property | Description |
|---|---|
id | Unique identifier for the action (e.g., 'duplicate', 'zoom-in') |
label | Translation key for display text. Can be a string or an object mapping contexts to different keys (see Context-sensitive labels) |
icon | Icon name from tldraw's icon set, or a custom React element |
kbd | Keyboard shortcut string. Use commas to bind several combinations, e.g. 'cmd+g,ctrl+g' (see Keyboard shortcuts) |
readonlyOk | When true, the action works in readonly mode. Defaults to false |
checkbox | When true, renders as a toggle with a checkmark indicator in menus |
isRequiredA11yAction | When true, the keyboard shortcut works even when shortcuts are normally disabled (e.g., while editing a shape). Used for accessibility actions |
isAvailable | Optional (editor) => boolean. When it returns false, menus hide the action (see Enabling and checking actions) |
isEnabled | Optional (editor) => boolean. When it returns false, menus disable or hide the action |
isChecked | Optional (editor) => boolean. For checkbox actions, whether the item shows as checked |
onSelect | Handler called when the action is triggered. Receives a TLUiEventSource indicating the trigger origin ('kbd', 'menu', 'toolbar', etc.) |
Accessing actions
Use the useActions hook to get all registered actions:
import { useActions } from 'tldraw'
function MyComponent() {
const actions = useActions()
return (
<button onClick={() => actions['undo'].onSelect('toolbar')}>
Undo
</button>
)
}The hook returns a record mapping action IDs to action objects. You can iterate over it to build custom menus or filter actions by property.
Default actions
The default actions cover most editing operations you'd expect in a canvas application. Some common ones, by category:
| Category | Action ids |
|---|---|
| Editing | undo, redo, duplicate, delete, copy, cut, paste |
| Grouping | group, ungroup |
| Arrangement | bring-to-front, bring-forward, send-backward, send-to-back, align-left, align-center-horizontal, align-right, distribute-horizontal, distribute-vertical |
| Export | export-as-svg, export-as-png, copy-as-svg, copy-as-png |
| Zoom | zoom-in, zoom-out, zoom-to-100, zoom-to-fit, zoom-to-selection, select-zoom-tool |
| Preferences | toggle-dark-mode, toggle-snap-mode, toggle-grid, toggle-focus-mode |
The full list is in the default actions source. Most actions guard themselves inside their handlers. For example, group and the arrangement actions do nothing unless shapes are selected and the select tool is active.
Overriding actions
Pass an overrides prop to customize actions. The override function receives the editor, the default actions, and helper utilities:
import { Tldraw, TLUiOverrides } from 'tldraw'
const overrides: TLUiOverrides = {
actions(editor, actions, helpers) {
// Modify existing action
actions['duplicate'].kbd = 'cmd+shift+d,ctrl+shift+d'
// Disable an action by removing it
delete actions['print']
return actions
},
}
function App() {
return <Tldraw overrides={overrides} />
}The copy, cut, and paste shortcuts are handled by native clipboard events rather than the kbd system, so changing their kbd has no effect.
Modifying behavior
To change what an action does, replace its onSelect handler:
const overrides: TLUiOverrides = {
actions(editor, actions, helpers) {
const originalDuplicate = actions['duplicate'].onSelect
actions['duplicate'].onSelect = async (source) => {
console.log('Duplicating shapes...')
await originalDuplicate(source)
console.log('Done!')
}
return actions
},
}You can call the original handler before or after your custom logic, or replace it entirely.
Adding custom actions
Add new actions by inserting them into the actions record:
const overrides: TLUiOverrides = {
actions(editor, actions, helpers) {
actions['my-custom-action'] = {
id: 'my-custom-action',
label: 'action.my-custom-action',
kbd: 'cmd+shift+k,ctrl+shift+k',
icon: 'external-link',
onSelect(source) {
const shapes = editor.getSelectedShapes()
console.log('Custom action on', shapes.length, 'shapes')
},
}
return actions
},
}Custom actions integrate with the keyboard shortcut system automatically. To add them to menus, override the menu components; see Actions in menus and the custom menus example.
Using helper utilities
The override function receives a helpers object (TLUiOverrideHelpers, the return value of useDefaultHelpers):
const overrides: TLUiOverrides = {
actions(editor, actions, helpers) {
actions['show-toast'] = {
id: 'show-toast',
label: 'action.show-toast',
onSelect(source) {
helpers.addToast({
title: 'Hello!',
description: 'This is a custom action.',
})
},
}
return actions
},
}Available helpers:
| Helper | Description |
|---|---|
addToast | Show a toast notification |
removeToast | Remove a specific toast |
clearToasts | Remove all toasts |
addDialog | Open a dialog |
removeDialog | Close a specific dialog |
clearDialogs | Close all dialogs |
msg | Get a translated string by key |
isMobile | Boolean indicating mobile breakpoint |
insertMedia | Open file picker and insert media |
replaceImage | Replace selected image with new file |
replaceVideo | Replace selected video with new file |
printSelectionOrPages | Print selection or all pages |
cut | Cut selected shapes to clipboard |
copy | Copy selected shapes to clipboard |
paste | Paste from clipboard |
copyAs | Copy shapes as SVG or PNG |
exportAs | Export shapes as SVG, PNG, or JSON |
getEmbedDefinition | Get embed info for a URL |
Keyboard shortcuts
Shortcuts use a simple string format with modifier keys separated by +. Use commas to bind several combinations to the same action. Every combination is active on every platform; the conventional cmd+…,ctrl+… pair covers Mac and everything else, and only the shortcut hint shown in menus (TldrawUiKbd) is platform-specific:
kbd: 'cmd+g,ctrl+g' // Cmd+G or Ctrl+G
kbd: 'shift+1' // Shift+1
kbd: 'cmd+shift+s,ctrl+shift+s' // Cmd+Shift+S or Ctrl+Shift+SModifiers are cmd (alias meta), ctrl, shift, and alt (alias option). Special keys include del, backspace, enter, escape, space, and the arrow keys (left, right, up, down).
To bind the + key itself, double the trailing plus: cmd++. That matches an unshifted + key (German, Nordic and similar layouts, or the numpad). On layouts where + is shift plus another key, such as US, write cmd+shift++, which is the same binding as cmd+shift+=.
Shortcuts match the character the browser reports for the key press, not the physical keycap, with two exceptions: shifted number-row keys match by position, so write them as shift+<digit> on every layout, and when the reported character is non-ASCII (Cyrillic, Greek, macOS Option dead keys) matching falls back to the physical key's US QWERTY character, so cmd+z still works on a Russian layout. With shift held, the shifted and unshifted US spellings are the same binding, so shift+: and shift+; both fire when the browser reports : with shift held. Bind what the keyboard emits, not what the keycap says: on French AZERTY the : key emits / with shift held, so bind shift+/; on Japanese JIS the ^ key emits ~, so bind shift+~. The characters !, ? and $ are reserved as legacy modifier markers and cannot be used as keys.
Shortcuts only fire while the editor is focused and the key event does not target a text input. They are also disabled when a menu is open, a shape is being edited, the editor has a crashing error, or the user has disabled keyboard shortcuts in preferences. In readonly mode, only actions with readonlyOk are bound. Actions marked with isRequiredA11yAction: true bypass the disabled check for accessibility purposes.
Actions in menus
The default UI uses TldrawUiMenuActionItem to render actions in menus:
import { TldrawUiMenuActionItem, TldrawUiMenuGroup } from 'tldraw'
function CustomMenu() {
return (
<TldrawUiMenuGroup id="edit">
<TldrawUiMenuActionItem actionId="undo" />
<TldrawUiMenuActionItem actionId="redo" />
<TldrawUiMenuActionItem actionId="duplicate" />
</TldrawUiMenuGroup>
)
}This component looks up the action by ID and renders it with the correct label, icon, and shortcut hint. If the action defines isEnabled, the item disables itself when it returns false. Pass whenDisabled="hide" to hide it instead, whether it's disabled by isEnabled or by your own disabled prop. In the context menu, a disabled TldrawUiMenuActionItem is hidden either way. For toggle actions, use TldrawUiMenuActionCheckboxItem; it reads checked from the action's isChecked unless you pass checked.
Context-sensitive labels
Some actions show different labels depending on where they appear. The label property can be an object mapping menu context types (TLUiMenuContextType, plus default) to translation keys:
actions['export-as-svg'] = {
...actions['export-as-svg'],
label: {
default: 'action.export-as-svg',
menu: 'action.export-as-svg.short',
'context-menu': 'action.export-as-svg.short',
},
}The menu component uses the appropriate label based on its context. If no specific label exists for a context, it falls back to default.
Enabling and checking actions
isEnabled tells every menu whether the action can run right now. isChecked tells checkbox items whether they're on. Both receive the editor and must not change anything. Menus re-run them when the editor state they read changes, so read everything they depend on from the editor; a value read from anywhere else won't update the menu.
actions['make-red'] = {
id: 'make-red',
label: 'action.make-red',
isEnabled: (editor) => editor.getSelectedShapeIds().length > 0,
onSelect() {
// ...
},
}Keyboard shortcuts ignore isAvailable and isEnabled, so onSelect should still check its own preconditions.
When you change a built-in action, spread the original so it keeps its isAvailable, isEnabled and isChecked:
actions['group'] = { ...actions['group'], onSelect: myGroup }Replacing an action with a new object drops them: menus show it as always enabled, and a checkbox item for it always shows as unchecked.
Use isAvailable instead when the action doesn't exist in the current context at all, rather than being blocked right now. Every menu item hides an unavailable action, whatever whenDisabled says; only the keyboard shortcuts dialog still lists it. The built-in paste (without clipboard read access), copy as PNG (without clipboard write access), copy as JSON (outside debug mode), cursor chat (on touch), exit pen mode, stop following, and back to content work this way.
actions['open-devtools'] = {
id: 'open-devtools',
label: 'action.open-devtools',
isAvailable: (editor) => editor.getInstanceState().isDebugMode,
onSelect() {
// ...
},
}To hide a submenu when none of its actions can run, use useSomeActionsEnabled. It returns true when any of the given actions is available and enabled; actions that aren't readonlyOk count as unavailable while the editor is readonly:
const IDS = ['make-red', 'make-blue']
function ColorsSubmenu() {
if (!useSomeActionsEnabled(IDS)) return null
return (
<TldrawUiMenuSubmenu id="colors" label="menu.colors">
{IDS.map((id) => (
<TldrawUiMenuActionItem key={id} actionId={id} whenDisabled="hide" />
))}
</TldrawUiMenuSubmenu>
)
}Tracking action usage
The source parameter tells you where the action was triggered. Use this for analytics:
actions['custom-action'] = {
id: 'custom-action',
label: 'action.custom',
kbd: 'cmd+k,ctrl+k',
onSelect(source) {
trackEvent('custom-action', { source })
// source: 'kbd', 'menu', 'context-menu', 'toolbar', 'quick-actions', 'zoom-menu', etc.
},
}Related examples
- Action overrides - Add custom actions and modify existing action shortcuts using the overrides prop.
- Keyboard shortcuts - Change keyboard shortcuts for tools and actions.
- Custom menus - Build custom menus that use actions with proper labels and shortcuts.