Skip to content

Task Item

The TaskItem node represents an individual checkbox list item (<li data-type="taskItem">) inside a TaskList. It renders a checkbox and a content area, tracks a checked attribute, and provides keyboard shortcuts for toggling, splitting, and indenting.

You don’t need to add TaskItem manually. It is automatically included when you add TaskList via its addExtensions(). If you are using StarterKit, TaskItem is already included.

import { Document, Text, Paragraph, TaskList } from '@domternal/core';
import { DomternalEditor } from '@domternal/vanilla';
const dm = new DomternalEditor(document.getElementById('editor')!, {
extensions: [Document, Text, Paragraph, TaskList],
content: `
<ul data-type="taskList">
<li data-type="taskItem" data-checked="false"><p>Unchecked task</p></li>
<li data-type="taskItem" data-checked="true"><p>Checked task</p></li>
</ul>
`,
});
PropertyValue
ProseMirror nametaskItem
TypeNode
GroupNone (used as content of TaskList)
Contentparagraph block* (strict: paragraph label + optional children-zone)
DefiningYes
HTML tag<li data-type="taskItem">

The first child paragraph is the label - aligned with the checkbox baseline. Heading-first content would break checkbox visual alignment, hence the strict schema.

The rendered HTML structure is:

<li data-type="taskItem" data-checked="false">
<label contenteditable="false">
<input type="checkbox">
</label>
<div>
<p><!-- label paragraph (required first child) --></p>
<!-- additional children-zone blocks (optional) -->
</div>
</li>

The <label> with contenteditable="false" prevents the cursor from entering the checkbox area. The <div> wrapper holds the editable content. In v0.7+ the checkbox click toggles the checked attribute via the NodeView (no longer requires Mod-Enter).

OptionTypeDefaultDescription
HTMLAttributesRecord<string, unknown>{}HTML attributes added to the <li> element
nestedbooleantrueWhether nested lists are allowed inside task items
import { TaskItem } from '@domternal/core';
const CustomTaskItem = TaskItem.configure({
HTMLAttributes: { class: 'my-task-item' },
});
AttributeTypeDefaultDescription
checkedbooleanfalseWhether the task is checked/completed

The checked attribute is stored as data-checked="true" or data-checked="false" on the <li> element, and also controls the checked attribute on the inner <input type="checkbox">.

When splitting a task item with Enter, the checked attribute is not carried over to the new item (keepOnSplit: false). New items always start unchecked.

CommandDescription
toggleTask()Toggle the checked state of the current task item
// Toggle the current task's checked state
editor.commands.toggleTask();
// With chaining
editor.chain().focus().toggleTask().run();

toggleTask finds the nearest taskItem ancestor of the cursor and flips its checked attribute.

ShortcutCursor positionBehavior
EnterIn label, non-emptySplit task item at the cursor; creates a new UNCHECKED item below
EnterIn label, emptyLift content out of the task list (exit)
EnterIn children-zone, non-emptySplit in place (both halves stay in same item)
EnterIn children-zone, empty paragraphInsert empty paragraph as sibling INSIDE the same task item
EnterOn a CHECKED task (label)Spawns an UNCHECKED sibling (new-work assumption)
BackspaceIn label at offset 0Lift task item (v0.6.0 behavior preserved)
BackspaceIn empty children-zone paragraphLift as top-level paragraph below the list
TabIn any task-item slotSink into nested list
Shift-TabIn any task-item slotLift out one level
Click checkbox-Toggle checked attribute via NodeView (NEW in v0.7.0)
Mod-Enter-Toggle checked state via command

The Enter behavior depends on whether the cursor is in the label paragraph (first child) or the children-zone (subsequent blocks):

In the label:

  1. Non-empty: Splits the task item, creating a new UNCHECKED item below (splitListItem with checked: false).
  2. Empty: Lifts the content out of the task list. If the item is the last one, Enter on the empty label exits the list and creates a paragraph below.
  3. Enter on a CHECKED task’s label (any position): The new sibling item is always UNCHECKED. The checked attribute is intentionally not forwarded (keepOnSplit: false) since the next task is new work.
  4. Empty inside a nested list within a regular list item: Escapes to the parent list level by creating a new list item at the correct depth.

In the children-zone:

  1. Empty paragraph: Inserts a new empty paragraph as a sibling INSIDE the same task item.
  2. Non-empty paragraph: Splits in place via splitBlock. Both halves stay in the same task item.

The checkbox click is now routed through a NodeView so users can toggle without using Mod-Enter or the toolbar. The NodeView’s stopEvent guard prevents PM from treating the click as a selection.

Strikethrough on checked tasks is scoped to the label paragraph only - nested children-zone content stays unstruck (theme update in v0.7.0).

Unlike ListItem which relies on ListKeymap for Tab/Shift-Tab, TaskItem handles these shortcuts directly using ProseMirror’s sinkListItem and liftListItem commands.

{
"type": "taskItem",
"attrs": { "checked": false },
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Unchecked task" }
]
}
]
}

A checked task item:

{
"type": "taskItem",
"attrs": { "checked": true },
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Completed task" }
]
}
]
}

@domternal/core - TaskItem.ts