---
title: Hotkeys
description: The default keymap, the chord syntax for plugin and app hotkeys, precedence, and how to override or disable a default binding.
icon: keyboard
---

Every key the editor handles goes through one keymap: your app's bindings, then each plugin's in list order, then the built-in ones. This page lists the defaults and shows how to add, override and disable bindings.

In the tables, <kbd>Mod</kbd> is <kbd>Cmd</kbd> on macOS and iOS and <kbd>Ctrl</kbd> elsewhere.

## Default keymap

### Editing and history

The **Command** column names the command `onBeforeOperation` sees first (see [commands and steps](/docs/plugins/operations#commands-and-steps)).

| Keys | Action | Command | Where |
| --- | --- | --- | --- |
| <kbd>Enter</kbd> | Split the block, or add a block before or after it; what it does depends on the block's role, see [Enter and Backspace by role](#enter-and-backspace-by-role) | `splitBlock`, `insertBlockAfter`, `insertBlockBefore`, `addChildBlock`, `unNestBlock` or `setBlock` | Text |
| <kbd>Shift</kbd> + <kbd>Enter</kbd> | Insert a soft line break | `insertText` | Text |
| <kbd>Mod</kbd> + <kbd>Enter</kbd> | Modify the block it is in, as in Notion: open or close a toggle (the document does not change); with the rich text plugin, check or uncheck a to-do. Over a block selection or a text range it acts on each such block the view shows, toggles and to-dos together; a to-do inside a collapsed toggle is left as it is. Elsewhere it does nothing: it never splits or edits text, and it claims the key, so a browser's own <kbd>Ctrl</kbd> + <kbd>Enter</kbd> paragraph break never runs | none; `setBlock` for a to-do | Text, or selected blocks |
| <kbd>Tab</kbd> | Nest the block into its previous sibling (after a list, under its last item; nothing happens when the list ends with a block that holds no children, such as an image, as under that block); selected sibling blocks, or the blocks a text selection spans, nest together and stay selected. A text selection spans the blocks holding its text: a list it starts or ends in is not one of them, its items are, so the keys move the same blocks as selecting those rows; a list it runs through moves whole with the blocks around it. Selected blocks with an unselected block between them move apart, so the keys never reorder the text. A selection across nesting levels nests each group of sibling blocks one level, as one undo step; a selected block's selected descendants move with it. A closed toggle a block nests into opens, and a moved toggle keeps its open state | `nestBlock`; `moveBlocks` for several | Text, or selected blocks |
| <kbd>Shift</kbd> + <kbd>Tab</kbd> | Move the block out one level, after its parent; the blocks nested after it become its children. An item of a list container leaves the list instead and never takes the items after it (see [list items](#enter-and-backspace-by-role)). Selected sibling blocks, or the blocks a text selection spans, move out together. Across nesting levels each group of sibling blocks moves out one level, a selected block's selected descendants with it; top-level blocks stay. A closed toggle that takes the blocks after it as children opens | `unNestBlock`; `moveBlocks` for several | Text, or selected blocks |
| <kbd>Backspace</kbd>, <kbd>Delete</kbd> | Delete the selected blocks, the selected atom, the selected range or one character; at a block edge, see [by role](#enter-and-backspace-by-role). With <kbd>Alt</kbd>, <kbd>Mod</kbd> or <kbd>Ctrl</kbd> (a word or line delete), selected blocks or a selected atom go exactly as without it, in every browser | `deleteBlocks`, `removeInlineBlock`, `deleteContentWithinSelection` or `deleteText` | Anywhere |
| <kbd>Mod</kbd> + <kbd>Z</kbd> | Undo | none (the [history](/docs/editor/history)) | Anywhere |
| <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>Z</kbd> | Redo | none | Anywhere |
| <kbd>Mod</kbd> + <kbd>Y</kbd> | Redo | none | Windows and Linux |

### Enter and Backspace by role

This table is the reference for what <kbd>Enter</kbd> and <kbd>Backspace</kbd> do at a block's edges. A block's role comes from its kind's record ([custom blocks](/docs/customization/blocks)); the bundled kinds are listed in [Blocks](/docs/concepts/blocks#bundled-kinds). A block can have more than one role: a toggle continues and is a container.

| Role | Kinds | <kbd>Enter</kbd> | <kbd>Backspace</kbd> at the start |
| --- | --- | --- | --- |
| Plain | paragraph, heading | At the end, a block of the parent's default child kind after it (`insertBlockAfter`); at the start, one before it (`insertBlockBefore`); in the middle, a split whose second half takes the default child kind (`splitBlock`). At the end of a block with text and children, a split that keeps the kind and takes the children. | A kind the menus offer (one with `presets`) other than the parent's default child, such as a heading, turns into that default, keeping its text and children (`setBlock`). Otherwise, a nested block that is its parent's last child moves out one level (`unNestBlock`), and any other merges into the block before it (`mergeBlockBackward`); a list's first item leaves the list (see List item). |
| Continuing (`continues`) | bulleted and numbered items, to-do, toggle | A block of the same kind with its first preset's data (a new to-do is unchecked), after it at the end and before it at the start; a split in the middle keeps the kind. At the end of one with text and children, a split whose second half keeps the kind, with its first preset's data (an unchecked to-do), and takes the children. In an empty one without children, it ends the run: it moves out one level when its parent is a continuing kind too (`unNestBlock`), else it turns into the parent's default child in place (`setBlock`). | As plain: turns into the parent's default child first. |
| Container (`container`) | toggle, callout, quote | At the end of one with children, or of an open toggle, a first child of its default child kind (`addChildBlock`). At the end of a closed toggle, a new toggle after it; its children stay. In the middle of the header of one with children, or of an open toggle, the text after the caret becomes its first child, of its default child kind, and the children stay with the header (`splitBlock`, one step); in a closed toggle's, it goes to a new toggle after it and the children stay. Otherwise as its other role. | As plain. A closed toggle is one unit, as in Notion: <kbd>Backspace</kbd> at the start of the block after it merges that block into the toggle's header (its children take its place), and <kbd>Delete</kbd> at the end of the header merges the block after the toggle, never the hidden children. Either is one command: a plugin that refuses it changes nothing. |
| List item (in a container that renders no content) | `list-item` in `ordered-list` and `unordered-list` (from JSON, the API or an Edytor fragment; an HTML paste gives flat bulleted and numbered items) | As plain: a new item after it, before it, or a split. In an empty item without children, it outdents the item one level (`unNestBlock`), as <kbd>Shift</kbd> + <kbd>Tab</kbd> does, until it ends the list: out of a list nested in an item it becomes an item under that item, then an item of the outer list after it, then a paragraph. Out of a list it lands before the list from its first item, after it from its last, and between the two halves of the list from a middle one. A list left with no item is removed. | The first item leaves the list the same way, before it, and the caret stays at its start (`mergeBlockBackward`); the last one moves out after it (`unNestBlock`); a middle one merges into the item before it. <kbd>Delete</kbd> at the end of the block before the list, a closed toggle's header included, pulls the first item's text up into that block (`mergeBlockForward`), and a list left with no item is removed. <kbd>Shift</kbd> + <kbd>Tab</kbd> lifts an item as an empty item's <kbd>Enter</kbd> does, and Turn into (the menus, the toolbar, the slash menu, a markdown shortcut) takes it out the same way, out of every list it sits in directly (a list nested right in a list): a list holds only items. Turning an item into the kind it shows as (Numbered list in an `ordered-list`, Bulleted list in an `unordered-list`, `1. ` or `- ` typed at its start) keeps it in the list, and the menus and the toolbar name it by that kind; a block a merge left in the list (a heading) turned into that kind becomes one of its items, and turned into its own kind (Heading 2 on a heading, Quote on a quote) it only changes in place. A divider or a code block (the menus, <kbd>Mod</kbd> + <kbd>Alt</kbd> + <kbd>8</kbd>, typing `---` or three backticks) takes an empty item's place out of the list the same way; after an item with text it goes right after the item, outside the list, which splits there. A vetoed conversion changes nothing, lift included. A pasted list of the list's own kind (an `<ol>` into an `ordered-list`) gives items. The list itself is never converted: its own grip offers no Turn into. See [Containers](/docs/concepts/blocks#containers). |
| Void (`void`) | divider, image | A void block's own text (an image caption) never splits. At the caption's end or start, a block after or before it. | Backspace at the start of the block after a void block selects it; a second <kbd>Backspace</kbd> deletes it. <kbd>Delete</kbd> at the end of the block before it does the same. A void block never merges. |
| Island (`island`) | code | Inside, a new line (a `codeLine`). | Lines merge inside the island only: the first line never joins the block before it, and with a single line, <kbd>Backspace</kbd> selects the whole island. With the code plugin, <kbd>Delete</kbd> at the end of the block before a code block removes that block when it is empty (the caret goes to the end of the text before it, or to the start of the code when nothing comes before) and does nothing otherwise, and <kbd>Backspace</kbd> in an empty block right after a code block removes that block and puts the caret at the end of the code. |

### Selection

| Keys                                          | Action                                                                                   | Where                         |
| --------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------- |
| <kbd>Mod</kbd> + <kbd>A</kbd>                   | Select the block's text; press again to select the block, then every block               | Anywhere                      |
| <kbd>↑</kbd>, <kbd>↓</kbd>                    | Select the previous or next block                                                        | One selected block            |
| <kbd>Shift</kbd> + <kbd>↑</kbd>, <kbd>Shift</kbd> + <kbd>↓</kbd> | Add the next block to the selection, or remove the last one when moving back | Block selection               |
| <kbd>Shift</kbd> + <kbd>↑</kbd>, <kbd>Shift</kbd> + <kbd>↓</kbd> | Extend the text selection one line; at the end of a block, <kbd>Shift</kbd> + <kbd>↓</kbd> selects a following void block | Text |
| <kbd>Escape</kbd>                             | Leave the block selection, caret at the end of the first selected block's line (an image's caption); over dividers alone, the start of the next line, else the end of the one before | Block selection               |
| <kbd>Enter</kbd>, <kbd>Shift</kbd> + <kbd>Enter</kbd> | Edit the text (Notion): leave the block selection, caret at the end of the first selected block, as <kbd>Escape</kbd> does; nothing is removed and `onDeleteSelectedBlocks` does not run. Over selected dividers alone, a new line after them | Block selection |

Arrow keys that walk a block selection do not enter an island (such as a code block) from outside, and they skip the hidden body of a closed toggle.

A text selection never reaches a closed toggle's hidden body. From inside its header to a block after it, <kbd>Backspace</kbd>, <kbd>Delete</kbd>, cut, typing, <kbd>Enter</kbd> and paste join what is left to the header and keep the body as its children; formatting and copy leave the body out. A paste of several lines, like <kbd>Enter</kbd>, leaves the body under the header and puts the rest in new blocks after it. When the selection starts at the header's start, <kbd>Backspace</kbd>, <kbd>Delete</kbd> and cut delete the toggle with its body, as they do a toggle the selection covers whole, and copy and formatting include the body; typing, <kbd>Enter</kbd> and paste keep the header and its body. When nothing else would be left, the toggle stays, empty and without its body. When the selection ends at the end of a closed toggle's header, the toggle goes and its body takes its place, shown, where the caret can land.

### Navigation

Every navigation key also has a <kbd>Shift</kbd> variant that extends the selection instead of moving the caret.

| Keys                                                   | Moves to                                   | Where              |
| ------------------------------------------------------ | ------------------------------------------ | ------------------ |
| <kbd>←</kbd>, <kbd>→</kbd>                             | The previous or next character, across blocks and atoms | Text  |
| <kbd>Alt</kbd> + <kbd>←</kbd>, <kbd>Alt</kbd> + <kbd>→</kbd> | The previous or next word                | macOS              |
| <kbd>Mod</kbd> + <kbd>←</kbd>, <kbd>Mod</kbd> + <kbd>→</kbd> | The previous or next word                | Windows and Linux  |
| <kbd>Home</kbd>, <kbd>End</kbd>                        | The start or end of the block              | Text               |
| <kbd>PageUp</kbd>, <kbd>PageDown</kbd>                 | The start or end of the document           | Text               |
| <kbd>Mod</kbd> + <kbd>↑</kbd>, <kbd>Mod</kbd> + <kbd>↓</kbd> | The start or end of the document         | Text               |
| <kbd>↑</kbd>, <kbd>↓</kbd>                             | The previous or next line (native)         | Text               |

Left, right and word keys follow the visual direction of right-to-left text, and they skip the body of a closed toggle. Over a block selection, the word, <kbd>Home</kbd>/<kbd>End</kbd> and document keys start from its shown edges: back to the start of its first shown line, forward to the end of its last: a selected block's own line, never a nested child that is not selected; for a block with no line of its own, such as a list or a code block, its first or last nested line; never a divider; over selected dividers alone they keep the dividers selected, except the document keys, which still go to the document's start or end.

### macOS text bindings

On macOS the Emacs-style <kbd>Ctrl</kbd> keys work as in native text fields:

| Keys                              | Action                                                      |
| --------------------------------- | ----------------------------------------------------------- |
| <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Ctrl</kbd> + <kbd>E</kbd> | Start or end of the block              |
| <kbd>Ctrl</kbd> + <kbd>B</kbd>, <kbd>Ctrl</kbd> + <kbd>F</kbd> | Previous or next character             |
| <kbd>Ctrl</kbd> + <kbd>P</kbd>, <kbd>Ctrl</kbd> + <kbd>N</kbd> | Like <kbd>↑</kbd> and <kbd>↓</kbd>     |
| <kbd>Ctrl</kbd> + <kbd>H</kbd>, <kbd>Ctrl</kbd> + <kbd>D</kbd> | Delete backward or forward             |
| <kbd>Ctrl</kbd> + <kbd>K</kbd>      | Delete to the end of the block; at the end, join the next block |
| <kbd>Ctrl</kbd> + <kbd>O</kbd>      | Insert a line break, keeping the caret before it            |

### Plugin keys

Rich text, arrow move and block handles are on by default in `<Edytor>`; the others apply when you list their plugin.

| Keys                                               | Action                                           | Plugin, where                            |
| -------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- |
| <kbd>Mod</kbd> + <kbd>B</kbd>, <kbd>I</kbd>, <kbd>U</kbd>, <kbd>E</kbd> | Bold, italic, underline, inline code | [Rich text](/docs/plugins/rich-text)     |
| <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd>, <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>X</kbd> | Strikethrough              | Rich text                                |
| <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd>       | Red text color                                   | Rich text                                |
| <kbd>Mod</kbd> + <kbd>Enter</kbd>                    | Check or uncheck the to-dos (one undo step)      | Rich text, a to-do selected or holding the caret |
| <kbd>Mod</kbd> + <kbd>Alt</kbd> + <kbd>0</kbd> to <kbd>8</kbd> | Turn into text, heading 1 to 3, to-do, bulleted list, numbered list, toggle, code (a block with text or children keeps them, and the code block is inserted after it; after a list's item, outside the list) | Rich text, when that kind's command exists |
| <kbd>Tab</kbd>                                     | Insert a tab at the caret, indent every line a selection touches, or accept a suggestion | [Code](/docs/plugins/code), in a code line |
| <kbd>Shift</kbd> + <kbd>Tab</kbd>                    | Remove one leading tab (or up to two spaces) from every line the selection touches | Code, in a code line                     |
| <kbd>Shift</kbd> + <kbd>Enter</kbd>                  | New code line                                    | Code, in a code line                     |
| <kbd>Mod</kbd> + <kbd>A</kbd>                        | Select the whole code block's text               | Code, until its text is selected         |
| <kbd>Escape</kbd>                                  | Dismiss a suggestion                             | Code, in a code line                     |
| <kbd>↑</kbd>, <kbd>↓</kbd>, <kbd>Enter</kbd>, <kbd>Escape</kbd> | Navigate, run, close                | [Slash menu](/docs/plugins/slash-menu), while open |
| <kbd>Mod</kbd> + <kbd>↑</kbd>, <kbd>Mod</kbd> + <kbd>↓</kbd> | Move the selected blocks up or down        | [Arrow move](/docs/plugins/arrow-move), block selection |
| <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↑</kbd>, <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↓</kbd> | Move the caret's block (or the selected blocks) up or down | Arrow move, text or block selection |
| <kbd>Mod</kbd> + <kbd>D</kbd>                        | Duplicate the selected block (the first one), or else the caret's block | [Block menu](/docs/plugins/block-menu)   |
| <kbd>Alt</kbd> + <kbd>↑</kbd> <kbd>↓</kbd> <kbd>→</kbd> <kbd>←</kbd> | Move the block up, down, in, out | [Block handles](/docs/plugins/block-handles), handle focused |

The <kbd>Mod</kbd> + <kbd>Alt</kbd> digits follow Notion's numbering and run the kind's `block.*` command, so <kbd>Mod</kbd> + <kbd>Alt</kbd> + <kbd>8</kbd> needs the code plugin. The block menu's own keys (arrows, <kbd>Enter</kbd>, <kbd>Escape</kbd>) belong to its search field, see [Block menu](/docs/plugins/block-menu#keys).

A plugin key only claims the key in the situation listed. Otherwise the key falls through to the next binding, for example <kbd>Mod</kbd> + <kbd>↓</kbd> to the document end when no block is selected, or <kbd>Mod</kbd> + <kbd>Enter</kbd> to the built-in binding (a toggle opens or closes, anything else is left alone) outside a to-do. With the arrow move plugin (a default), <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↑</kbd>/<kbd>↓</kbd> moves a movable block instead of extending the selection to the document edge.

## Adding hotkeys

A plugin declares bindings in `hotkeys`. The app can pass its own with the `hotKeys` prop of `<Edytor>`. Both take the same functions:

```ts
import type { Plugin } from 'edytor';

export const savePlugin: Plugin = () => ({
	hotkeys: {
		'mod+s': ({ edytor, prevent }) => {
			prevent(() => {
				localStorage.setItem('doc', JSON.stringify(edytor.value));
			});
		}
	}
});
```

A binding receives `{ edytor, prevent, event? }`:

- `prevent()` claims the key: later bindings do not run, and the browser's default action and propagation are stopped. `prevent(cb)` claims it and runs `cb`. Like every `prevent`, it throws, so code after it does not run.
- A binding that does not call `prevent` leaves the key to the next binding, and finally to the browser.
- `event` is the `KeyboardEvent`. It is absent when the key arrived only as an input event, as with some virtual keyboards on Android.

The `hotKeys` prop is read once, when the editor mounts.

### Chord syntax

A chord is lowercase modifiers and one key, joined with `+`:

- **Modifiers**: `mod`, `alt`, `ctrl`, `shift`, up to three. Their order does not matter: `shift+mod+k` and `mod+shift+k` are the same chord. Case does not matter either at runtime, but the types expect lowercase.
- **Keys**: the letters `a` to `z`, the digits `0` to `9`, the punctuation keys (`/`, `.`, `[`, `+`, …, as `KeyboardEvent.key` names them), `arrowup`, `arrowdown`, `arrowleft`, `arrowright`, `tab`, `enter`, `backspace`, `delete`, `space`, `escape`, `home`, `end`, `pageup`, `pagedown`, `insert`, `f1` to `f12`. `space` is the space bar.
- Other names (`cmd`, `meta`, `option`, `esc`, `return`, `del`, `up`…) never match a key press. They fail type-checking, and in development the editor logs a warning naming the chord.
- `mod` is <kbd>Cmd</kbd> on Apple platforms and <kbd>Ctrl</kbd> elsewhere. `ctrl` matches the <kbd>Ctrl</kbd> key only on Apple platforms; elsewhere <kbd>Ctrl</kbd> is always `mod`.
- On non-Latin keyboard layouts, a <kbd>Mod</kbd> chord also matches by physical key: <kbd>Mod</kbd> + <kbd>Б</kbd> on a Russian layout runs `mod+b`.
- <kbd>AltGr</kbd> combinations and dead keys produce text and never run a binding.
- A punctuation key is the character the key press types. A character typed with <kbd>Shift</kbd> needs `shift` in the chord: <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>/</kbd> is `mod+shift+?`, so `mod+?` and `shift+/` never match. On macOS, <kbd>Option</kbd> with a character key types a character (<kbd>Option</kbd> + <kbd>B</kbd> is `∫`), so `alt+b` never matches there; bind `mod+alt+b` instead. In development the editor logs a warning for these chords too.

A plugin's `hotkeys` and the `hotKeys` prop are type-checked against this syntax (`HotKeyCombination`).

<kbd>Enter</kbd>, <kbd>Shift</kbd> + <kbd>Enter</kbd>, <kbd>Backspace</kbd> and <kbd>Delete</kbd> reach bindings too, so a plugin can claim them, as the slash menu does with <kbd>Enter</kbd>. Each key press runs its bindings once.

## Precedence

For each chord, the bindings run in this order until one calls `prevent`:

1. the `hotKeys` prop of `<Edytor>`;
2. each plugin's `hotkeys`, in the order of the `plugins` array;
3. the built-in bindings.

A binding that is not a function, such as an optional callback left `undefined` (`'mod+e': onInlineCode`), is skipped: the chord's next binding runs, and in development the editor logs a warning naming the chord. TypeScript accepts `undefined` for these optional keys, so add an optional callback only when it is set: `...(onInlineCode && { 'mod+e': onInlineCode })`.

## Overriding and disabling defaults

To override a default, bind the same chord in the `hotKeys` prop or in a plugin, and call `prevent`:

```svelte
<script lang="ts">
	import { Edytor } from 'edytor';

	let saved = $state(0);
</script>

<Edytor
	hotKeys={{
		// Mod+Enter saves (the default only opens toggles and checks to-dos).
		'mod+enter': ({ prevent }) => prevent(() => saved++),
		// Tab never nests: claim it and do nothing.
		tab: ({ prevent }) => prevent(),
		// Mod+B does nothing in headings; elsewhere the rich text binding runs.
		'mod+b': ({ edytor, prevent }) => {
			if (edytor.selection.state.startBlock?.type === 'heading') prevent();
		}
	}}
/>
```

`prevent()` without a callback disables the key in the editor. It also prevents the browser's default, so a disabled <kbd>Tab</kbd> does not move focus out of the editor either. There is no way to remove a built-in binding and hand the key back to the browser.
