Content controls

Find, fill, create, and lock DOCX content controls from a server script or inside the editor.

DOCX content controls (w:sdt) mark fillable regions. Tag them in a template, then find each control by tag, title, or id. The editor preserves unsupported properties, such as w15:repeatingSection, on save.

Content controls are not {{ mustache }} template variables. The two systems can coexist in one document.

Fill a control from code

@docx-editor.dev/editor-api fills controls on a server. Headless automation requires an EigenPal Pro License. Load the tags, then commit all writes in one final sync:

import { readFile, writeFile } from 'node:fs/promises';
import { DocxEditor } from '@docx-editor.dev/editor-api';

const runtime = await DocxEditor.createServer(await readFile('contract-template.docx'));
try {
  await runtime.run(async (context) => {
    const controls = context.document.body.contentControls;
    controls.load('items/tag');
    await context.sync(); // Read the control tags.

    for (const control of controls.items) {
      if (control.tag === 'customerName') control.setValue({ kind: 'text', text: 'Acme GmbH' });
      if (control.tag === 'effective') control.setValue({ kind: 'date', iso: '2026-07-01' });
      if (control.tag === 'agree') control.setValue({ kind: 'checkbox', checked: true });
      if (control.tag === 'betaClause') control.delete(false); // condition not met
    }
    await context.sync(); // Commit all writes together.
  });

  await writeFile('contract-acme.docx', Buffer.from(await runtime.save()));
} finally {
  runtime.dispose();
}

getByTag(tag) narrows the collection. getById(id) and getFirstOrNullObject() address one control. For signatures and batching rules, see Content controls in the Editing API.

A value write keeps the enclosing cell, row, and paragraph formatting:

  • Text, list, and date writes replace one paragraph's content with one run and remove its sibling paragraphs.
  • The kept paragraph is the first one with text, or the first one if none has text. It keeps its bookmarks and comment anchors.
  • Checkbox writes change only the state glyph.

A write that would remove a sibling table or nested control, cross a field, or discard unsupported markup fails and leaves the document unchanged.

Choose a value operation

insertText() and delete() are Office.js members. setValue() is a DocxEditor extension, not an Office.js member.

OperationKindUse for
insertText(text, 'Replace' | 'Start' | 'End')Office.jsFree text.
delete(keepContent)Office.jsRemove the control with its content, or unwrap it.
setValue({ kind: 'text', text })ExtensionRich-text and plain-text controls.
setValue({ kind: 'listItem', value })ExtensionDropdowns and combo boxes. Must match a declared item.
setValue({ kind: 'checkbox', checked })ExtensionCheckbox controls.
setValue({ kind: 'date', iso })ExtensionDate pickers. YYYY-MM-DD or a full ISO-8601 instant.

Load properties before you read them. placeholderShown is true while the control shows its prompt instead of entered data:

controls.load('items/placeholderShown');
await context.sync();

const unfilled = controls.items.filter((c) => c.placeholderShown);

cannotEdit and cannotDelete expose the control's locks. Set them when you author a template.

Inspect and create controls

useContentControl() reports the control at the caret and whether you can write it:

import { useContentControl } from '@docx-editor.dev/react';

function ControlInspector() {
  const { control, setValue, canSetValue, setValueDisabledReason } = useContentControl();
  if (!control) return null;

  return (
    <button
      type="button"
      onMouseDown={(event) => event.preventDefault()}
      disabled={!canSetValue}
      title={setValueDisabledReason ?? undefined}
      onClick={() => setValue('Acme GmbH')}
    >
      Fill {control.alias ?? control.tag ?? control.id}
    </button>
  );
}
MemberPurpose
controltag, alias, id, controlType, locked, removalLocked, effectiveLock, bound, placeholder.
setValue, canSetValue, setValueDisabledReasonWrite the value, or read why the write is refused.
remove, canRemoveRemove the control.
showAll, setShowAllShow boundaries for every control.
formFill, setFormFillTab and Shift+Tab move between editable controls.

The packaged ContentControl part uses this hook. CONTENT_CONTROL_SLOTS lists its chrome slot IDs.

Run insertContentControl to create a control as one undo step. It wraps the selection, or inserts an empty control with a prompt:

editor.exec({
  type: 'insertContentControl',
  subtype: 'plainText',
  tag: 'customerName',
  title: 'Customer name',
});

subtype accepts richText, plainText, dropdown, comboBox, and date. Pass target: { paraId, search } to wrap a phrase other than the selection. New dropdowns and combo boxes have no items.

editor.can() returns the same refusal as exec():

OperationRefusal conditionReason
Fill or editThe control or an ancestor locks its content.locked
RemoveThe control or an ancestor locks its wrapper, or an enclosing control locks its content.locked
FillThe control declares w:dataBinding.bound
FillThe value does not match the control type.typeMismatch
CreateThe selection crosses paragraphs.Inline controls cannot wrap block content.
CreateThe position is inside a hyperlink, field, or inline control.No valid sibling position.

Configure locks and form navigation

These creation options and props change lock behavior. The editor reads each one once, at creation:

OptionEffect
lockedControlNavigation: 'atomic'Content-locked inline controls become one caret unit.
contentLockPolicy: { allowFormattingInLockedContent: true }Run formatting passes the content lock.

formFill changes Tab navigation only. It does not prevent edits outside controls. Use document protection to restrict edits.

Caret movement in content-locked controls

By default, the caret walks through each character of a content-locked inline control. Set lockedControlNavigation to 'atomic' to step over it:

const editor = createDocxEditor({
  container,
  document: bytes,
  lockedControlNavigation: 'atomic',
});

React and Vue accept the same lockedControlNavigation prop. With 'atomic', arrows step over a contentLocked or sdtContentLocked control, and a press inside selects the whole control.

  • The option applies in the body, headers, and footers. Notes and text boxes keep character navigation.
  • Selections set from code, such as setSelection, stay where you put them.
  • The option changes navigation only. Locks refuse the same edits.
  • With pointer: 'native', the browser places the caret. Pointer gestures do not treat the control as one unit.

Formatting content-locked controls

A content lock refuses text edits and character formatting with code locked. It also refuses paragraph styles, lists, text direction, tab stops, and contextual spacing. Alignment, indents, spacing, and break settings stay available. In suggesting mode, they refuse with locked when the paragraph already has a suggested change that records a locked property, such as a style.

Set contentLockPolicy to let authors style locked text:

const editor = createDocxEditor({
  container,
  document: bytes,
  contentLockPolicy: { allowFormattingInLockedContent: true },
});

const host = createServerAutomationHost(bytes, {
  contentLockPolicy: { allowFormattingInLockedContent: true },
});

React and Vue accept the same contentLockPolicy prop. Bold, italic, color, font, size, and highlight then pass the lock.

  • Typing, deletion, paragraph styles, and lists still refuse with locked.
  • Tracked character formatting in suggesting mode still refuses.
  • Data-bound controls still refuse character formatting with bound.
  • The option does not remove saved locks or binding restrictions. The saved w:lock value stays the same.
  • The option applies to local edits only. Collaborators receive permitted formatting even when their own policy refuses it.

Customize value entry

Replace the value popups through these popups entries:

Popup entryControls
contentControlThe control inspector
contentControlWidgetDropdown, combo box, date, and building block gallery
contentControlCheckboxCheckbox
contentControlPicturePicture

Omit an entry to keep the default popup. DocxEditorContentControlWidget provides composable parts, and useContentControlWidget() exposes draft state and actions. For examples, see Compose the value popup.

Style the default menus with the docx-content-control-menu, docx-content-control-menu-item, docx-content-control-calendar-*, and docx-content-control-widget classes and --doc-* tokens. The date picker follows the editor's locale.

Limits

  • Data-bound controls show the bound value when the document opens. Direct writes refuse with bound.
  • Commands that create, fill, or remove controls do not create revisions.
  • In suggesting mode, delete gestures on a contentLocked control propose one tracked deletion.
  • Picture picks reject files larger than 32 MiB. Image replacement refuses in suggesting mode.
  • Canceling, closing the editor, or entering suggesting mode during decoding prevents the picture write.
  • Picture replacement keeps the drawing's size, crop, name, and alt text. A different aspect ratio can stretch.
  • Controls that wrap a whole table cell or row work like other controls. Value writes keep the table structure.
  • Controls inside a hyperlink are not surfaced.
  • Typing at the end of an unlocked text control stays inside it. Typing at the end of a hyperlink inside a control leaves only the hyperlink.
  • Repeating sections round-trip without item add, remove, or configuration edits.
  • Building block galleries list only blocks in the document's own glossary part.
  • An inline control accepts only a one-paragraph building block. A block with several paragraphs or a table needs a block-level control.
  • A gallery pick supports self-contained text, direct formatting, math, tables, and unbound nested controls. A block that references styles, numbering, media, relationships, bookmarks, notes, fields, or revisions returns unsupported, even when the main document has a style with the same name.
  • Forms protection permits edits inside unlocked controls. Read-only and comments-only protection refuse control writes.
  • For legacy form fields, see Legacy text form fields.
  • Binding resolution and glossary support follow DOCX fidelity.

Data-bound controls refresh their value from the bound store:

AspectBehavior
StoresCustom XML parts in the package, and the core and extended document properties.
XPathAbsolute paths of child elements, with optional positions such as [2]. The last step can select an element, an attribute, or text().
Plain text valuesSingle-line controls drop line breaks. Multiline controls keep them.
Checkbox valuestrue, false, 1, or 0.
Saved content keptRich text controls, controls with no type, unsupported values, and unsupported XPath.
CollaborationThe participant that creates the room refreshes the controls. Other participants use the room's content.

See also