Engineering Journal
Pdf Processor
Pdf Processor

document.execCommand('indent') silently does nothing outside a list

2026-07-11

TLDR

Executing document.execCommand('indent') on standard paragraph text (<p>) fails silently in most modern browsers without throwing an error or returning false. The browser implementation has defined behavior only for list items (<li>), where it nests elements into sub-lists (<ul>/<ol>). To support paragraph indentation, inspect selection DOM nodes before calling execCommand and insert a pre-formatted tab span (<span class="tab-char">\t</span>) for non-list selections.
Selection ContextNative execCommand('indent') BehaviorCustom Fallback StrategyRendered Output
List Item (<li>)Nests <li> inside child <ul>Native execCommand('indent')Nested list structure
Paragraph (<p>)Silent No-Op (No DOM change)Insert <span class="tab-char">\t</span>Visual tab indent preserved

Technical defect analysis

Rich text editor toolbars frequently bind UI indent buttons directly to browser DOM APIs:

// DEFECTIVE IMPLEMENTATION: Fails silently outside list contexts
function onIndentButtonClicked() {
  document.execCommand('indent'); // Silent no-op when selection is in <p> or <div>!
}

Because execCommand('indent') lacks standardized DOM transformation behavior for non-list block elements, browsers exit the execution loop without mutating the DOM or indicating failure.

Inserting a raw \t text node into standard HTML flow collapses into a single space unless styled explicitly.


Remediation: context-aware indentation logic

Inspect selection context and insert a styled tab span (white-space: pre) when executing outside list structures:

export function executeContextAwareIndent() {
  const selection = window.getSelection();
  if (!selection || selection.rangeCount === 0) return;

const range = selection.getRangeAt(0); const containerNode = range.commonAncestorContainer; const element = containerNode.nodeType === Node.ELEMENT_NODE ? containerNode : containerNode.parentElement;

// Case 1: Selection lives inside a list item -> Native execCommand works if (element && element.closest('li')) { document.execCommand('indent'); return; }

// Case 2: Selection lives inside paragraph/block copy -> Insert explicit tab span const tabSpan = document.createElement('span'); tabSpan.className = 'tab-char'; tabSpan.textContent = '\t';

range.collapse(true); range.insertNode(tabSpan);

// Move cursor position immediately after inserted tab span range.setStartAfter(tabSpan); range.setEndAfter(tabSpan); selection.removeAllRanges(); selection.addRange(range); }

Required CSS rule

/ Ensure browser renders tab character width instead of collapsing to single space /
.tab-char {
  white-space: pre;
}
Rule of thumb: Inspect selection node context before calling execCommand and provide explicit DOM fallback transformations for non-list selections.
Read this post in the full Engineering Journal →