Related
No open issue covers a gap between two lists. Issue 376 is a different Backspace bug. It deletes an image that sits under an empty line. Do not treat that issue as this bug.
What happened
Edward Saperia reported this on 2026-09-26. He wrote: "there's some slightly unintuitive behaviour with lists. When you're editing a list, it's easy to create a gap:"
He also wrote: "This is a paragraph break maybe? but you can't join the two lists together again."
A bullet list can split into two lists, with a blank gap between them. He cannot join the two lists again.
His screenshot shows Venue, Event Calendar, House London WhatsApp group, and Wi-Fi. Wi-Fi has nested SSID and Password. After the gap come Planning Permission Data, Google Drive, GitHub, and Data Asset Register. The CLI cannot attach that screenshot. Please drag it onto this issue.
What we expect
Backspace or Delete in the empty gap joins two lists of the same type into one list. The gap does not remain as an empty item. No key throws.
The exact key rule is a maintainer decision. The help pages in Reference points do not state it.
Steps to reproduce
- Open a pad you can edit.
- Make a bullet list and nest one item under the last item.
- Leave the list so an empty paragraph sits under it. Start a second bullet list under that paragraph.
- Put the caret in the empty paragraph. Press Backspace. Press Backspace again.
- Undo. Put the caret at the end of the last nested item before the gap. Press Delete.
Expected after the first Backspace: one list, and no empty item. Reported: the lists stay two lists, and the empty paragraph sits inside the last item. Reported on Delete: the editor throws.
The key results below were not seen in a browser. They come from the headless script in Findings.
Findings
A colleague measured this on 2026-09-29 with a headless Jest jsdom script. That script is deleted. This task did not re-run it. The script built new Editor with the pad list stack and pressed editor.commands.keyboardShortcut(...). The same results came from a plain StarterKit editor.
The pad stack is StarterKit with listKeymap: false, then ListKeymapWithoutTab, ParagraphStyle, TaskList, TaskItem, TitleDocument, and Indent. ListKeymapWithoutTab only drops the Tab key. TipTap.tsx sets listKeymap: false and adds that extension. The installed list package is @tiptap/extension-list 3.31.3.
Fixture: UL[Venue, Wi-Fi[SSID, Password]] | empty paragraph | UL[Planning, Drive[Nested]].
- Backspace in the empty gap moves the empty paragraph into the last list item as a second paragraph. The two lists stay two lists. The caret jumps to the start of the first item of the second list. A second Backspace lifts that item out into a plain paragraph. That split grows.
- Delete in the empty gap removes the paragraph. The two lists stay two lists. The caret lands at the start of the second list. A second Delete does nothing.
- Backspace at the start of the first item after the gap lifts that item out to a paragraph. A second Backspace deletes the empty gap. The end state is
UL | p"Planning" | UL.
- Delete at the end of the last nested item before the gap throws
TransformError: Inconsistent open depths. The stack runs from @tiptap/core keymap.ts handleDelete through commands.first (joinForward) into prosemirror-transform 1.12.1 Transform.step.
toggleBulletList on the gap paragraph joins the lists and leaves an empty bullet where the gap was. That is the only re-join path the script found.
toggleBulletList on a middle item lifts that item out and splits the list into UL | p | UL. Toggling again re-joins it. Shift-Tab on an empty top-level middle item lifts it out and leaves UL | empty p | UL. Enter twice at the end of a nested last item only outdents. A third Enter adds another empty item and does not exit the list.
Code read on 2026-09-29, and not re-run:
handleBackspace in @tiptap/extension-list 3.31.3 does not look at a nested list on the previous item. If the caret is not in a list item and a list sits before it, the handler cuts the current block. The cut lands in the last list-item descendant. It then runs joinForward. If the caret is at the start of a list item's first child, the handler runs liftListItem. listItemHasSubList sits in the same folder. handleBackspace does not import it. The document result of that nested case was not measured.
handleDelete returns false when the caret is not in a list item. The core keymap in @tiptap/core keymap.ts then tries joinForward. In prosemirror-commands 1.7.2, joinForward calls deleteBarrier. @tiptap/pm 3.31.3 resolves that commands package and prosemirror-transform 1.12.1.
No spec under apps/webapp/cypress/e2e/editor/lists/ covers a gap or a join. list-edge-cases.cy.js documents exit on outdent. Shift-Tab at the top level of a bullet list leaves a paragraph.
Reference points
apps/webapp/src/components/TipTap/listKeymapWithoutTab.ts
apps/webapp/src/components/TipTap/TipTap.tsx
apps/webapp/cypress/e2e/editor/lists/list-edge-cases.cy.js
- Installed
@tiptap/extension-list 3.31.3 src/keymap/listHelpers/handleBackspace.ts
- Installed
prosemirror-commands 1.7.2 src/commands.ts
- ProseMirror
joinBackward
- Google Docs lists
- Word for the web lists
Google Docs help says Tab starts a list inside a list. Enter twice goes back to the main list. That page does not say what Backspace does on a blank line between two lists.
Word for the web help says Enter twice finishes the list. Backspace deletes the last bullet or number. That page does not say what a blank line between two lists does.
joinBackward runs when the selection is empty and the caret is at the start of a textblock. It tries to join that block to the block before it. If it cannot join, it lifts the block or moves it toward the previous parent. deleteBarrier tries joinMaybeClear first. It then tries to wrap the following node into the previous node. It then tries to lift the following block. It then tries to pull the following textblock into the previous textblock. joinForward uses the same barrier in the other direction.
Acceptance criteria
Blocked by
None — can start now.
Agent brief
Type: HITL for the join rule. AFK for the handlers and the tests after that comment.
Category: bug
Current behavior: See Findings. An empty paragraph between two lists does not join them. Backspace can push that paragraph into the last item. Delete at one nested boundary throws.
Desired behavior: Two lists of the same type can be joined again. No key throws. The empty gap does not become an empty item inside a list.
Where to start: A small pad extension with Backspace and Delete handlers for the empty gap paragraph is only a candidate. Read handleBackspace in @tiptap/extension-list 3.31.3 before adding a handler, so the two do not fight. apps/webapp/src/components/TipTap/listKeymapWithoutTab.ts. apps/webapp/src/components/TipTap/TipTap.tsx. Search by symbol. File names are hints as of 2026-09-29.
Rules that apply: apps/webapp/src/components/TipTap/CLAUDE.md §Editor Architecture. AGENTS.md §Test Policy allows these tests, because they pin a reported bug. A Cypress key press needs real keys, and the editor must have focus first. Do not turn ListKeymapWithoutTab into a second list behavior unless the candidate extension is rejected.
Verify: Start make dev-local. Confirm the webapp port first. Rebuild the split list, make the gap, and press Backspace and Delete. Then run the new list spec with bun run --filter @docs.plus/webapp cypress:run. Add --spec and limit it to the new file.
Related
No open issue covers a gap between two lists. Issue 376 is a different Backspace bug. It deletes an image that sits under an empty line. Do not treat that issue as this bug.
What happened
Edward Saperia reported this on 2026-09-26. He wrote: "there's some slightly unintuitive behaviour with lists. When you're editing a list, it's easy to create a gap:"
He also wrote: "This is a paragraph break maybe? but you can't join the two lists together again."
A bullet list can split into two lists, with a blank gap between them. He cannot join the two lists again.
His screenshot shows Venue, Event Calendar, House London WhatsApp group, and Wi-Fi. Wi-Fi has nested SSID and Password. After the gap come Planning Permission Data, Google Drive, GitHub, and Data Asset Register. The CLI cannot attach that screenshot. Please drag it onto this issue.
What we expect
Backspace or Delete in the empty gap joins two lists of the same type into one list. The gap does not remain as an empty item. No key throws.
The exact key rule is a maintainer decision. The help pages in Reference points do not state it.
Steps to reproduce
Expected after the first Backspace: one list, and no empty item. Reported: the lists stay two lists, and the empty paragraph sits inside the last item. Reported on Delete: the editor throws.
The key results below were not seen in a browser. They come from the headless script in Findings.
Findings
A colleague measured this on 2026-09-29 with a headless Jest jsdom script. That script is deleted. This task did not re-run it. The script built
new Editorwith the pad list stack and pressededitor.commands.keyboardShortcut(...). The same results came from a plainStarterKiteditor.The pad stack is
StarterKitwithlistKeymap: false, thenListKeymapWithoutTab,ParagraphStyle,TaskList,TaskItem,TitleDocument, andIndent.ListKeymapWithoutTabonly drops the Tab key.TipTap.tsxsetslistKeymap: falseand adds that extension. The installed list package is@tiptap/extension-list3.31.3.Fixture:
UL[Venue, Wi-Fi[SSID, Password]] | empty paragraph | UL[Planning, Drive[Nested]].UL | p"Planning" | UL.TransformError: Inconsistent open depths. The stack runs from@tiptap/corekeymap.tshandleDeletethroughcommands.first(joinForward) intoprosemirror-transform1.12.1Transform.step.toggleBulletListon the gap paragraph joins the lists and leaves an empty bullet where the gap was. That is the only re-join path the script found.toggleBulletListon a middle item lifts that item out and splits the list intoUL | p | UL. Toggling again re-joins it. Shift-Tab on an empty top-level middle item lifts it out and leavesUL | empty p | UL. Enter twice at the end of a nested last item only outdents. A third Enter adds another empty item and does not exit the list.Code read on 2026-09-29, and not re-run:
handleBackspacein@tiptap/extension-list3.31.3 does not look at a nested list on the previous item. If the caret is not in a list item and a list sits before it, the handler cuts the current block. The cut lands in the last list-item descendant. It then runsjoinForward. If the caret is at the start of a list item's first child, the handler runsliftListItem.listItemHasSubListsits in the same folder.handleBackspacedoes not import it. The document result of that nested case was not measured.handleDeletereturns false when the caret is not in a list item. The core keymap in@tiptap/corekeymap.tsthen triesjoinForward. Inprosemirror-commands1.7.2,joinForwardcallsdeleteBarrier.@tiptap/pm3.31.3 resolves that commands package andprosemirror-transform1.12.1.No spec under
apps/webapp/cypress/e2e/editor/lists/covers a gap or a join.list-edge-cases.cy.jsdocuments exit on outdent. Shift-Tab at the top level of a bullet list leaves a paragraph.Reference points
apps/webapp/src/components/TipTap/listKeymapWithoutTab.tsapps/webapp/src/components/TipTap/TipTap.tsxapps/webapp/cypress/e2e/editor/lists/list-edge-cases.cy.js@tiptap/extension-list3.31.3src/keymap/listHelpers/handleBackspace.tsprosemirror-commands1.7.2src/commands.tsjoinBackwardGoogle Docs help says Tab starts a list inside a list. Enter twice goes back to the main list. That page does not say what Backspace does on a blank line between two lists.
Word for the web help says Enter twice finishes the list. Backspace deletes the last bullet or number. That page does not say what a blank line between two lists does.
joinBackwardruns when the selection is empty and the caret is at the start of a textblock. It tries to join that block to the block before it. If it cannot join, it lifts the block or moves it toward the previous parent.deleteBarriertriesjoinMaybeClearfirst. It then tries to wrap the following node into the previous node. It then tries to lift the following block. It then tries to pull the following textblock into the previous textblock.joinForwarduses the same barrier in the other direction.Acceptance criteria
apps/webapp/cypress/e2e/editor/lists/. It fails before the fix.Blocked by
None — can start now.
Agent brief
Type: HITL for the join rule. AFK for the handlers and the tests after that comment.
Category: bug
Current behavior: See Findings. An empty paragraph between two lists does not join them. Backspace can push that paragraph into the last item. Delete at one nested boundary throws.
Desired behavior: Two lists of the same type can be joined again. No key throws. The empty gap does not become an empty item inside a list.
Where to start: A small pad extension with Backspace and Delete handlers for the empty gap paragraph is only a candidate. Read
handleBackspacein@tiptap/extension-list3.31.3 before adding a handler, so the two do not fight.apps/webapp/src/components/TipTap/listKeymapWithoutTab.ts.apps/webapp/src/components/TipTap/TipTap.tsx. Search by symbol. File names are hints as of 2026-09-29.Rules that apply:
apps/webapp/src/components/TipTap/CLAUDE.md§Editor Architecture.AGENTS.md§Test Policy allows these tests, because they pin a reported bug. A Cypress key press needs real keys, and the editor must have focus first. Do not turnListKeymapWithoutTabinto a second list behavior unless the candidate extension is rejected.Verify: Start
make dev-local. Confirm the webapp port first. Rebuild the split list, make the gap, and press Backspace and Delete. Then run the new list spec withbun run --filter @docs.plus/webapp cypress:run. Add--specand limit it to the new file.