blokhaus

ListPlugin

Bullet lists, numbered lists, and checklists.

Import

import { ListPlugin } from "@blokhaus/core";

Overview

The ListPlugin provides bullet lists, numbered lists, and task/checklist support. It wraps Lexical's @lexical/list module and integrates with the Input Rule Engine for markdown-style triggers (- , 1. , [] , [x] ). The plugin handles list-specific keyboard behaviors including Enter to create new items, Enter on empty to exit the list, and Tab/Shift+Tab for indentation.

Props

The ListPlugin accepts no props. All list behavior is handled automatically once the plugin is mounted.

Usage

app/editor/page.tsx
"use client";

import { EditorRoot, ListPlugin, InputRulePlugin } from "@blokhaus/core";

export default function EditorPage() {
  return (
    <EditorRoot namespace="my-editor">
      <InputRulePlugin />
      <ListPlugin />
    </EditorRoot>
  );
}

Including InputRulePlugin alongside ListPlugin enables the markdown-style triggers. Without InputRulePlugin, lists can still be created programmatically or via the slash menu, but typing - will not automatically convert to a list.

Markdown triggers

These triggers are handled by the InputRulePlugin when ListPlugin is mounted:

TriggerProduces
- or * Unordered (bullet) list item
1. Ordered (numbered) list item
[] Task list item (unchecked)
[x] Task list item (checked)

Keyboard behavior

KeyContextAction
EnterNon-empty list itemCreate a new list item below
EnterEmpty list itemExit the list and create a paragraph
TabAny list itemIndent the item (increase nesting level)
Shift+TabAny list itemOutdent the item (decrease nesting level)
BackspaceStart of first list itemConvert back to a paragraph

Nesting example

- Item A
  - Nested under A (Tab from Item A's child)
    - Deeply nested (Tab again)
  - Another nested item
- Item B

Lists can be nested up to 10 levels deep. Attempting to indent beyond the maximum depth is silently ignored.

Task lists

Task list items render a checkbox alongside the text content. Clicking the checkbox toggles the checked state via a single editor.update() call, creating a clean undo history entry.

Creating task lists programmatically
import { INSERT_CHECK_LIST_COMMAND } from "@lexical/list";

editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);

Checked state in JSON

The checked state is persisted in the Lexical JSON state as part of the ListItemNode:

{
  "type": "listitem",
  "checked": true,
  "children": [{ "type": "text", "text": "Buy groceries" }]
}

Programmatic list creation

import {
  INSERT_UNORDERED_LIST_COMMAND,
  INSERT_ORDERED_LIST_COMMAND,
  INSERT_CHECK_LIST_COMMAND,
} from "@lexical/list";

// Create a bullet list
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);

// Create a numbered list
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);

// Create a task list
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);

Slash menu integration

When ListPlugin is mounted, the following items appear in the slash menu:

ItemDescription
Bullet ListInsert an unordered list
Numbered ListInsert an ordered list
Task ListInsert a checklist with toggleable items

Converting between list types

To convert an existing list from one type to another, dispatch the appropriate command while the cursor is inside the list. Lexical handles the conversion automatically, preserving the list items and their content.

// Convert current list to ordered
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);

The ListPlugin wraps @lexical/list and registers the necessary list nodes (ListNode and ListItemNode) automatically. You do not need to add them to the ALL_NODES array manually.

Pressing Enter on an empty list item at any nesting level first outdents the item. Only when the item is at the top level and empty does Enter exit the list entirely. This matches the behavior of editors like Notion and Google Docs.