List Keymap
ListKeymap provides keyboard shortcuts for manipulating list items: Tab to indent (sink), Shift-Tab to outdent (lift), and Backspace at the start of a list item to lift it out of the list. It works with bullet lists and ordered lists but does not interfere with task lists, which use their own item type.
Included in StarterKit by default.
When to use
Section titled “When to use”Use ListKeymap when you need:
- Backspace at the start of the first list item to lift it out of the list
- Backspace on an empty paragraph directly below a list to place the caret at the end of that list, instead of ProseMirror re-absorbing the paragraph as a new list item
- Backspace on an empty paragraph between two lists of the same type to merge them back into one
Notes:
- ListKeymap is included automatically by
BulletListandOrderedList: both addListItem, andListItemaddsListKeymap.TaskListdoes not: it addsTaskItem, which ships its own Tab / Shift-Tab / Enter / Backspace handlers, so a task-list-only editor has no ListKeymap. - Enter behavior for list items is handled by ListItem (and by TaskItem for task lists). ListKeymap registers no Enter handler.
ListKeymap is included in StarterKit, so it works out of the box. To use it standalone:
import { Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap,} from '@domternal/core';import { DomternalEditor } from '@domternal/vanilla';import '@domternal/theme';
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap], content: '<ul><li>First item</li><li>Second item (press Tab to indent)</li></ul>',});import { Component, signal } from '@angular/core';import { DomternalEditorComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap,} from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap]; content = '<ul><li>First item</li><li>Second item (press Tab to indent)</li></ul>';}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap,} from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap]} content="<ul><li>First item</li><li>Second item (press Tab to indent)</li></ul>" > <Domternal.Content /> </Domternal> );}<script setup lang="ts">import { Domternal } from '@domternal/vue';import { Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap } from '@domternal/core';
const extensions = [Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap];</script>
<template> <Domternal :extensions="extensions" content="<ul><li>First item</li><li>Second item (press Tab to indent)</li></ul>"> <Domternal.Content /> </Domternal></template>import { Editor, Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap,} from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, BulletList, OrderedList, ListItem, ListKeymap], content: '<ul><li>First item</li><li>Second item (press Tab to indent)</li></ul>',});To configure ListKeymap in StarterKit:
StarterKit.configure({ listKeymap: { listItem: 'listItem' },})To disable ListKeymap in StarterKit:
StarterKit.configure({ listKeymap: false })Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
listItem | string | 'listItem' | Name of the list item node type that these shortcuts apply to |
import { ListKeymap } from '@domternal/core';
const editor = new Editor({ extensions: [ ListKeymap.configure({ listItem: 'listItem', }), ],});Commands
Section titled “Commands”ListKeymap does not register any commands. It uses ProseMirror’s sinkListItem and liftListItem commands from prosemirror-schema-list directly in the keyboard shortcut handlers.
Keyboard shortcuts
Section titled “Keyboard shortcuts”| Key | Action | Description |
|---|---|---|
Tab | Sink (indent) | Nests the current list item inside the previous sibling, creating a sub-list |
Shift-Tab | Lift (outdent) | Moves the current list item one level up in the list hierarchy |
Backspace | Lift at start | When the cursor is at the very start of a list item with an empty selection, lifts the item out of the list |
Backspace | Exit-join below a list | When the cursor is at the start of an empty paragraph whose previous sibling is a bulletList, orderedList or taskList, deletes the paragraph and places the caret at the end of the last text block of that list |
Tab (indent)
Section titled “Tab (indent)”Pressing Tab inside a list item indents it one level, wrapping it in a new sub-list under the previous sibling item. This uses ProseMirror’s sinkListItem command.
Before: After Tab on "Second":- First item - First item- Second item - Second item- Third item - Third itemIf there is no previous sibling (the item is first in the list), Tab does nothing.
Shift-Tab (outdent)
Section titled “Shift-Tab (outdent)”Pressing Shift-Tab moves the list item one level up. This uses ProseMirror’s liftListItem command.
Before: After Shift-Tab on "Nested":- First item - First item - Nested item - Nested itemIf the item is already at the top level, Shift-Tab lifts it out of the list entirely, converting it to a paragraph.
When a list item is nested inside an item of a different kind (for example a bullet item in a task item’s children-zone), Shift-Tab preserves the item’s own kind instead of adopting the parent list’s: a task item keeps its checkbox and checked state, a bullet item stays a bullet. The item lifts out as a fresh list of its own type, splitting the surrounding list when needed.
Backspace (lift at start)
Section titled “Backspace (lift at start)”Pressing Backspace when the cursor is at the very start of a list item (empty selection, parentOffset === 0) lifts the item out of its parent list. As with Shift-Tab, a cross-type-nested item keeps its own kind and (for a task) its checked state rather than dissolving into the parent list’s type. This only triggers when:
- The selection is empty (collapsed cursor)
- The cursor is at position 0 within the parent text block
- The cursor is at the start of the list item’s first child (within 1 position of the list item’s start)
Backspace (exit-join below a list)
Section titled “Backspace (exit-join below a list)”When the cursor sits at the start of an empty paragraph whose previous sibling is a list group (bulletList, orderedList or taskList), Backspace deletes that paragraph and puts the caret at the end of the last text block of the preceding list. Without this, ProseMirror’s joinBackward would wrap the paragraph back into the list as a fresh item, producing the Enter / Backspace / Backspace ping-pong.
If that empty paragraph sat between two list groups of the same type, the two lists are joined back into one (validated with canJoin before the merge), so a list that was split by lifting a middle item is reunified.
Before (cursor on the empty line): After Backspace:- First item - First item- Second item - Second item|| - Third item- Third itemThis branch runs before the lift check, and unlike Tab / Shift-Tab it also applies after a task list, because the cursor is in a plain paragraph rather than inside a task item.
Input rules
Section titled “Input rules”ListKeymap does not register any input rules.
Toolbar items
Section titled “Toolbar items”ListKeymap does not register any toolbar items.
How it works
Section titled “How it works”Cross-type protection
Section titled “Cross-type protection”ListKeymap includes a guard (getListItemContext) that prevents its shortcuts from interfering with other list-like structures such as task lists. Before executing any shortcut, the function walks up the document tree from the cursor position:
- If it finds the target
listItemtype first, the shortcut runs - If it encounters a different defining block node (like
taskItem) inside a list parent first, the shortcut returnsfalseand does nothing
This ensures that Tab/Shift-Tab inside a task list is handled by the TaskItem extension, not by ListKeymap.
ProseMirror commands
Section titled “ProseMirror commands”The sinkListItem and liftListItem commands come from @domternal/pm/schema-list (ProseMirror’s prosemirror-schema-list package). They handle the structural tree transformations needed to nest and unnest list items while preserving content and maintaining valid document structure.
Companion: ListIndent (v0.7.0)
Section titled “Companion: ListIndent (v0.7.0)”ListKeymap covers Tab / Shift-Tab for cursors inside a list. The complementary ListIndent extension (new in v0.7.0) handles Tab / Shift-Tab at the boundary between top-level blocks and lists:
- Tab on a top-level paragraph immediately after a list moves it INTO the previous list as a nested child.
- Shift-Tab on a nested last-child block at the end of the last item lifts it OUT as a top-level paragraph.
Register ListIndent AFTER ListKeymap so ListIndent’s keymap runs first and defers to ListKeymap for in-list flows. Together they cover every Tab/Shift-Tab case in and around lists.
Exports
Section titled “Exports”import { ListKeymap } from '@domternal/core';import type { ListKeymapOptions } from '@domternal/core';| Export | Type | Description |
|---|---|---|
ListKeymap | Extension | The list keymap extension |
ListKeymapOptions | TypeScript type | Options for ListKeymap.configure() |
Source
Section titled “Source”@domternal/core - ListKeymap.ts
See also
Section titled “See also”- List Indent - Tab/Shift-Tab at the boundary between top-level blocks and lists (opt-in)
- Bullet List - unordered list with bullet markers
- Ordered List - numbered list with
1.markdown shortcut - Task List - checkbox to-do list container