Tiptap extension for character-based indentation: Tab inserts indentChars (default two spaces) at the caret or at the start of each selected line, and Shift-Tab removes it.
Tab already belongs to lists (sink/lift), tables (cell navigation), and the browser (focus). This extension resolves the key in one predictable order — list, table, then literal indent — and runs literal indent only in contexts you allowlist via allowedIndentContexts: paragraphs under doc or blockquote by default, with headings, code blocks, and every other textblock excluded until you add a rule.
bun add @docs.plus/extension-indentRequires @tiptap/core ^3.22.3 and @tiptap/pm ^3.22.3 (Tiptap 3.x).
Optional: @tiptap/extension-table enables cell navigation on Tab; list extensions (listItem / taskItem) enable sink/lift before literal indent.
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Indent } from '@docs.plus/extension-indent'
new Editor({
extensions: [StarterKit, Indent.configure({ indentChars: '\t' })]
})Indent.configure({}) merges with the defaults in Options.
| Option | Type | Default | Description |
|---|---|---|---|
indentChars |
string |
' ' (two spaces) |
Inserted or removed per step (often '\t'). |
enabled |
boolean |
true |
Disable behavior without removing the extension. |
allowedIndentContexts |
Array<{ textblock, parent }> |
paragraph under doc / blockquote |
Full allowlist for literal indent/outdent — see allowedIndentContexts. |
editor.commands.indent()
editor.commands.outdent()Both respect enabled and the same allowedIndentContexts rules as the keyboard path.
| Shortcut | Action |
|---|---|
Tab |
sinkListItem (listItem / taskItem) → goToNextCell (when a table extension is loaded) → indent() |
Shift-Tab |
liftListItem → goToPreviousCell → outdent() |
The extension registers at priority 25, below the Tiptap default of 100, so other extensions' own Tab handlers win first. When none of the three steps applies, the handler returns false and the keypress falls through to other extensions and the browser default — Tab focus navigation keeps working.
Literal indent() / outdent() run only when the innermost textblock at the caret (or at each line of a selection) and its immediate parent match one of the rules. Each rule is { textblock: string, parent: string } (Tiptap / ProseMirror NodeType.name).
The list is a full allowlist, not a merge: passing allowedIndentContexts to configure() replaces the default array, so list every pair you need.
| You want | Rules |
|---|---|
| Body + blockquote paragraphs (package default) | { textblock: 'paragraph', parent: 'doc' } and { textblock: 'paragraph', parent: 'blockquote' } |
| Body paragraphs only | Only paragraph + doc |
| Blockquote paragraphs only | Only paragraph + blockquote |
| List item paragraphs | { textblock: 'paragraph', parent: 'listItem' } and/or taskItem |
| Table cell paragraphs | { textblock: 'paragraph', parent: 'tableCell' } (if your schema uses it) |
| Headings | e.g. { textblock: 'heading', parent: 'doc' } — type name is lowercase heading, not HTML H1 |
Pass [] to turn off literal indent/outdent everywhere — Tab still sinks lists and moves table cells.
allowedNodeTypes (a flat list of type names matched against the node at the caret, where an empty list allowed every context) is gone. Map each name to one pair per parent you need:
// 0.1.x
Indent.configure({ allowedNodeTypes: ['paragraph'] })
// 2.x
Indent.configure({
allowedIndentContexts: [{ textblock: 'paragraph', parent: 'doc' }]
})[] now disables literal indent instead of allowing it everywhere.
Selected ranges split into visual lines using doc.textBetween(from, to, '\n'). Every line must match allowedIndentContexts; otherwise the command returns false and the document is unchanged. Lines indent and outdent at their starts, even when the selection begins or ends mid-line — select-all included.
With an empty selection, outdent() removes:
- the line's leading
indentCharswhen the caret sits at the start of an indented line, or - one
indentCharsimmediately before the caret elsewhere — undoes a just-inserted tab without moving to column 0.
Exports (all named): Indent, IndentOptions, IndentContextRule. Configure with Indent.configure({ indentChars, allowedIndentContexts }).
Sibling packages: extensions/README.md.
Bug reports and PRs welcome. Setup, test commands, and the playground harness live in CONTRIBUTING.md.
MIT — see LICENSE.