Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Hotkeys

The default keymap, the chord syntax for plugin and app hotkeys, precedence, and how to override or disable a default binding.

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, Mod is Cmd on macOS and iOS and Ctrl elsewhere.

Default keymap

Editing and history

The Command column names the command onBeforeOperation sees first (see commands and steps).

Keys Action Command Where
Enter 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 splitBlock, insertBlockAfter, insertBlockBefore, addChildBlock, unNestBlock or setBlock Text
Shift + Enter Insert a soft line break insertText Text
Mod + Enter Start a new block below, splitting at the end of the caret’s text. In a toggle’s header, open or close the toggle instead (Notion); the document does not change splitBlock Text
Tab Nest the block into its previous sibling; selected sibling blocks, or the blocks a text selection spans, nest together and stay selected. 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
Shift + Tab Move the block out one level, after its parent; the blocks nested after it become its children. 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
Backspace, Delete Delete the selected blocks, the selected atom, the selected range or one character; at a block edge, see by role deleteBlocks, removeInlineBlock, deleteContentWithinSelection or deleteText Anywhere
Mod + Z Undo none (the history) Anywhere
Mod + Shift + Z Redo none Anywhere
Mod + Y Redo none Windows and Linux

Enter and Backspace by role

This table is the reference for what Enter and Backspace do at a block’s edges. A block’s role comes from its kind’s record (custom blocks); the bundled kinds are listed in Blocks. A block can have more than one role: a toggle continues and is a container.

Role Kinds Enter Backspace 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).
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: Backspace at the start of the block after it merges that block into the toggle’s header (its children take its place), and Delete at the end of the header merges the block after the toggle, never the hidden children.
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 Backspace deletes it. Delete 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, Backspace selects the whole island. With the code plugin, Delete 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 Backspace 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
Mod + A Select the block’s text; press again to select the block, then every block Anywhere
↑, ↓ Select the previous or next block One selected block
Shift + ↑, Shift + ↓ Add the next block to the selection, or remove the last one when moving back Block selection
Shift + ↑, Shift + ↓ Extend the text selection one line; at the end of a block, Shift + ↓ selects a following void block Text
Escape Leave the block selection, caret at the end of the first selected block 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.

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

Keys Moves to Where
←, → The previous or next character, across blocks and atoms Text
Alt + ←, Alt + → The previous or next word macOS
Mod + ←, Mod + → The previous or next word Windows and Linux
Home, End The start or end of the block Text
PageUp, PageDown The start or end of the document Text
Mod + ↑, Mod + ↓ The start or end of the document Text
↑, ↓ 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.

macOS text bindings

On macOS the Emacs-style Ctrl keys work as in native text fields:

Keys Action
Ctrl + A, Ctrl + E Start or end of the block
Ctrl + B, Ctrl + F Previous or next character
Ctrl + P, Ctrl + N Like ↑ and ↓
Ctrl + H, Ctrl + D Delete backward or forward
Ctrl + K Delete to the end of the block; at the end, join the next block
Ctrl + O 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
Mod + B, I, U, E Bold, italic, underline, inline code Rich text
Mod + Shift + S, Mod + Shift + X Strikethrough Rich text
Mod + Shift + H Red text color Rich text
Mod + Enter Check or uncheck the to-do Rich text, in a to-do
Mod + Alt + 0 to 8 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) Rich text, when that kind’s command exists
Tab Insert a tab at the caret, indent every line a selection touches, or accept a suggestion Code, in a code line
Shift + Tab Remove one leading tab (or up to two spaces) from every line the selection touches Code, in a code line
Shift + Enter New code line Code, in a code line
Mod + A Select the whole code block’s text Code, in a non-empty code line
Escape Dismiss a suggestion Code, in a code line
↑, ↓, Enter, Escape Navigate, run, close Slash menu, while open
Mod + ↑, Mod + ↓ Move the selected blocks up or down Arrow move, block selection
Mod + Shift + ↑, Mod + Shift + ↓ Move the caret’s block (or the selected blocks) up or down Arrow move, text or block selection
Mod + D Duplicate the selected block (the first one), or else the caret’s block Block menu
Alt + ↑ ↓ → ← Move the block up, down, in, out Block handles, handle focused

The Mod + Alt digits follow Notion’s numbering and run the kind’s block.* command, so Mod + Alt + 8 needs the code plugin. The block menu’s own keys (arrows, Enter, Escape) belong to its search field, see Block menu.

A plugin key only claims the key in the situation listed. Otherwise the key falls through to the next binding, for example Mod + ↓ to the document end when no block is selected, or Mod + Enter to the built-in new block outside a to-do. With the arrow move plugin (a default), Mod + Shift + ↑/↓ 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:

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 Cmd on Apple platforms and Ctrl elsewhere. ctrl matches the Ctrl key only on Apple platforms; elsewhere Ctrl is always mod.
  • On non-Latin keyboard layouts, a Mod chord also matches by physical key: Mod + Б on a Russian layout runs mod+b.
  • AltGr 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 Shift needs shift in the chord: Mod + Shift + / is mod+shift+?, so mod+? and shift+/ never match. On macOS, Option with a character key types a character (Option + B 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).

Enter, Shift + Enter, Backspace and Delete reach bindings too, so a plugin can claim them, as the slash menu does with Enter. 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:

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

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

<Edytor
	hotKeys={{
		// Mod+Enter saves instead of starting a new block.
		'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 Tab 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.

Was this page helpful?