Content controls

Find and fill Word content controls by tag or id. Set text, dropdown, checkbox, and date values, from inside the editor or from a server-side script.

Word content controls (w:sdt, structured document tags) mark regions you can fill from code. Create a template in Word, tag its fillable regions, and use tag, title, or id to find each control.

The editor displays control boundaries and supports the editing operations below. It preserves unsupported properties, including w:dataBinding and w15:repeatingSection, when you save.

Content controls are addressed by tag, title, or id. They are not {{ mustache }} template variables; the two systems can coexist in one document.

See the Word fidelity matrix for support levels across features.

Filling a template from a server

@docx-editor.dev/editor-api is the headless path, and its object model is Office.js-compatible. Reads are batched, then one sync() sends the writes as a single ordered batch:

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();
    await context.sync(); // one round trip: now you know what the template has

    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(); // one atomic batch: all of the writes, or none
  });

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

getByTag(tag) narrows the collection directly when you know what you are looking for, and getById(id) / getFirstOrNullObject() address a single control.

Values are typed

setValue takes a discriminated value rather than a bare string, so a typed control cannot be filled with something it cannot hold:

KindShapeFor
text{ kind: 'text', text }Rich-text and plain-text controls.
listItem{ kind: 'listItem', value }Dropdowns and combo boxes. Must match a declared item.
checkbox{ kind: 'checkbox', checked }Checkbox controls.
date{ kind: 'date', iso }Date pickers. YYYY-MM-DD or a full ISO-8601 instant.

insertText(text, 'Replace' | 'Start' | 'End') writes free text where that is what you want, and delete(keepContent) either drops a control with its content or unwraps it and keeps the content in place.

Reading state before writing

placeholderShown tells you the control still holds Word's boilerplate ("Click here to enter text") rather than real data. Check it before treating text as entered content. cannotEdit and cannotDelete expose the control's locks, and are settable when you are producing a template rather than filling one.

controls.load();
await context.sync();

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

In the editor

useContentControl() is the live equivalent. It reports the control at the caret, whether it can be written, and why not when it cannot:

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

function ControlInspector() {
  const { control, setValue, canSetValue, setValueDisabledReason, remove, canRemove } =
    useContentControl();

  if (!control) return null;

  return (
    <div>
      <h4>{control.alias ?? control.tag ?? control.id}</h4>
      <p>{control.controlType}</p>
      <button
        disabled={!canSetValue}
        title={setValueDisabledReason ?? undefined}
        onClick={() => setValue('Acme GmbH')}
      >
        Fill
      </button>
      <button disabled={!canRemove} onClick={() => remove()}>
        Remove
      </button>
    </div>
  );
}

The inspector state carries tag, alias, id, controlType, locked, removalLocked, effectiveLock, bound (whether it is driven by a data binding), and placeholder.

showAll / setShowAll toggles boundary rendering for every control, and formFill / setFormFill puts the document into fill-only mode, where the caret can enter controls but not the text around them. The packaged ContentControl part is the inspector panel over exactly this API, and CONTENT_CONTROL_SLOTS lists the matching chrome slot ids.

A data-bound control rejects a direct write: its content comes from the Custom XML store, so writing it here would not persist in Word. setValueDisabledReason reports the reason instead of dropping the write.

Creating a control in the editor

insertContentControl authors a new control in the open document, as one undoable step. Select text to wrap it in a control, or leave the caret where it is to insert an empty control that shows Word's prompt for its type:

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

function InsertFieldButton() {
  const editor = useDocxEditor();

  return (
    <button
      type="button"
      onMouseDown={(event) => event.preventDefault()}
      onClick={() =>
        editor?.exec({
          type: 'insertContentControl',
          subtype: 'plainText',
          tag: 'customerName',
          title: 'Customer name',
        })
      }
    >
      Insert field
    </button>
  );
}

tag is the identity you look the control back up by, and title is the label Word shows. An empty control holds its prompt until the user types: the first character replaces the whole prompt, the way it does in Word.

To address text other than the selection, pass a target that names a paragraph by its paraId and the phrase inside it:

editor.exec({
  type: 'insertContentControl',
  target: { paraId: '1B4C77A2', search: 'Acme GmbH' },
  subtype: 'plainText',
  tag: 'customerName',
});

You can author richText, plainText, dropdown, comboBox, and date controls. OOXML also spells dropdown as dropDownList.

A new dropdown or combo box has no items. Add its items in Word, or fill a control from an existing template.

editor.can() answers the same refusal exec would, so a button can disable itself and show the reason.

OperationRefusal conditionReason
Fill or editThe control or an ancestor locks its contentlocked
RemoveThe control or an ancestor locks its wrapperlocked
FillThe control declares w:dataBindingbound
FillThe value does not match the control typetypeMismatch
CreateThe selection crosses paragraphsInline controls cannot wrap block content
CreateThe position is inside a hyperlink, field, or inline controlThe new control has no valid sibling position

Checkbox, dropdown, and date interactions

Select the button at the upper-right corner of a control to toggle a checkbox, open a dropdown menu, or open a date picker. Each edit supports undo and requires no application event handler. Checkbox toggles use MS Gothic when the document omits the state font.

These interactions apply to content controls. For legacy Word form fields, see Legacy text form fields.

Operational limits

  • The editor does not support a control around a whole table cell or row. Controls inside a cell work.
  • The editor does not surface a control inside a hyperlink.
  • w:dataBinding round-trips without bound-value resolution.
  • Repeating-section markup round-trips without item add, item remove, or section configuration edits.
  • Control creation, filling, and removal do not create revisions in suggesting mode.

Next steps

On this page