Content controls
Find and fill Word content controls by tag, alias, or id. Set text, dropdown, checkbox, and date values and manage repeating sections, headless or live.
Word content controls (w:sdt, Structured Document Tags) are labeled, bounded regions of a document. A block-level control wraps one or more paragraphs or a table and carries a stable tag, alias, and id. That stability makes them the natural anchor for templates, conditional sections, and document automation: design the template in Word, tag the fillable regions, then fill them by tag from code.
The editor parses block controls into the document model, keeps them editable, renders their boundary, and round-trips them losslessly on save, including unmodeled properties such as w:dataBinding and w15:repeatingSection.
Content controls are addressed by tag, alias, or id. They are not the {{mustache}} template variables handled by the docxtemplater plugin; the two systems coexist in one document.
Finding controls
Headless (no DOM, no editor), against a parsed Document:
import {
parseDocx,
findContentControls,
findContentControl,
} from "@eigenpal/docx-editor-core/headless";
const doc = await parseDocx(buffer);
const all = findContentControls(doc); // ContentControlInfo[]
const checkboxes = findContentControls(doc, { type: "checkbox" });
const intro = findContentControl(doc, { tag: "intro" }); // first match or undefinedThe filter is { tag?, alias?, id?, type? }. Each ContentControlInfo carries tag, alias, id, sdtType, lock, listItems, placeholder, text, plus modeled state: showingPlaceholder, checked, dateFormat, and dataBinding.
When showingPlaceholder is true, text is the placeholder boilerplate ("Click here to enter text"), not entered content. Check this flag before treating text as real data.
Setting text content
setContentControlContent fills a richText or plainText control with a string (or BlockContent[] headless):
import { setContentControlContent, removeContentControl } from "@eigenpal/docx-editor-core/headless";
let next = setContentControlContent(doc, { tag: "intro" }, "Filled by template");
// Conditional sections: drop a control, or unwrap it (keep its content)
next = removeContentControl(next, { tag: "optionalClause" });
next = removeContentControl(next, { tag: "wrapper" }, { keepContent: true });All headless mutators are pure: they return a new Document and preserve the control's identity and raw w:sdtPr, so the result still round-trips. Mutators affect the first match only; when a tag repeats, enumerate with findContentControls and disambiguate by id.
Safety rules (each overridable with { force: true }):
- A content-locked control throws
ContentControlLockedErroron edit; a deletion-locked one throws on remove. - A typed control (dropdown, date, checkbox, picture, group) throws
ContentControlTypeErroron free-text replacement. Use the typed setter below. - A data-bound control (
w:dataBinding) throwsContentControlBoundError: its content is driven by the Custom XML store, so a direct write would not persist in Word. - An unmatched filter throws
ContentControlNotFoundError.
Setting typed values (dropdown, checkbox, date)
setContentControlValue updates both the visible content and the structured w:sdtPr state:
import { setContentControlValue } from "@eigenpal/docx-editor-core/headless";
setContentControlValue(doc, { tag: "status" }, { kind: "dropdown", value: "2" });
setContentControlValue(doc, { tag: "agree" }, { kind: "checkbox", checked: true });
setContentControlValue(doc, { tag: "effective" }, { kind: "date", date: "2026-06-01" });A dropdown value must match one of the control's list items (by value or display text). A date is ISO yyyy-mm-dd and is rendered with the control's w:dateFormat. comboBox controls are pick-only today; typing a value outside the list is not supported.
In the editor, typed controls also get an interactive trigger at the top-right of their box: it toggles the checkbox, opens the dropdown's item menu, or opens a date picker, each as a normal undoable edit. No wiring required, in both the React and Vue adapters.
Repeating sections
Repeating-section controls (w15:repeatingSection) round-trip verbatim, and two helpers manage their items:
import { addRepeatingSectionItem, removeRepeatingSectionItem } from "@eigenpal/docx-editor-core/headless";
// Clone an item (with fresh ids); afterIndex picks the insertion point.
let next = addRepeatingSectionItem(doc, { tag: "lineItems" }, { afterIndex: 0 });
// Drop one item by index.
next = removeRepeatingSectionItem(next, { tag: "lineItems" }, 2);Unwrapping a repeating-section control is refused, since it would orphan the w15 structure.
In the live editor
The same operations exist on DocxEditorRef, running against the editor so writes are normal undoable edits:
const ref = useRef<DocxEditorRef>(null);
ref.current?.getContentControls({ type: "dropDownList" }); // PMContentControl[]
ref.current?.scrollToContentControl({ tag: "intro" });
ref.current?.setContentControlContent({ tag: "intro" }, "Filled"); // boolean
ref.current?.setContentControlValue({ tag: "agree" }, { kind: "checkbox", checked: true });
ref.current?.removeContentControl({ tag: "optionalClause" });These return false when no control matches; locked and typed controls throw under the same rules as headless unless { force: true } is passed. The ref's setContentControlContent takes a string (newlines become paragraphs).
Headless document generation
A complete fill-and-export pipeline in Node:
import { readFile, writeFile } from "node:fs/promises";
import {
parseDocx,
setContentControlContent,
setContentControlValue,
removeContentControl,
repackDocx,
} from "@eigenpal/docx-editor-core/headless";
const buffer = await readFile("contract-template.docx");
let doc = await parseDocx(buffer);
doc = setContentControlContent(doc, { tag: "customerName" }, "Acme GmbH");
doc = setContentControlValue(doc, { tag: "effective" }, { kind: "date", date: "2026-07-01" });
doc = removeContentControl(doc, { tag: "betaClause" }); // condition not met
const out = await repackDocx(doc);
await writeFile("contract-acme.docx", Buffer.from(out));Current limits
- Scope is block-level controls in the document body. Inline controls parse and round-trip but are not part of this addressing API.
- Controls inside headers/footers and inside table cells are not discovered;
findContentControlswill not return them and mutators report "not found". dataBindingand repeating sections round-trip verbatim but have no live behavior (no bound-value resolution, no automatic repeat expansion).
Next steps
- Headless processing for the full server-side toolkit
- Core headless API for every exported helper
- AI & Agents for filling documents from an LLM
Comments
Add comment threads to DOCX documents: replies, resolve state, the comments sidebar, controlled comment props and callbacks, and programmatic APIs.
Headers & footers
Edit DOCX headers and footers in place, insert PAGE and NUMPAGES fields, use per-section first-page and even-page variants, and add watermarks.