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 provides typed operations to read and edit controls. 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.

Writing a control value preserves its enclosing table cell, row, and formatting after saving and reopening. Text, dropdown, combo box, and date values replace the display paragraph's content with one run. The editor selects the first paragraph with text, or the first paragraph if none has text, checking cells with text first. It keeps that paragraph's properties, bookmarks, and comment anchors and removes sibling paragraphs. Checkbox updates change only the state-glyph run.

The editor rejects a value update if it would remove a sibling table or nested control, cross a field boundary, or discard unsupported markup. A failed update leaves the document unchanged.

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

Each dropdown, combo box, date, building block gallery, and picture control shows a button past its right edge, the way Word attaches its tab. The button appears while the pointer or the caret is on the control, while its menu is open, and under show-all, and it stays reachable by keyboard the whole time. It scales with the page zoom. A press on the prompt of a dropdown, combo box, date, or gallery control selects the prompt and opens its menu in one step. A checkbox control's button covers its glyph. Select the button to toggle a checkbox, open a dropdown menu, open a date picker, list the document's building blocks, or choose an image file. The menu opens under the control's left edge, flips above when space is limited, and stays inside the visible page sheet. A menu closes when you press outside it, press the control button again, or press Escape. Each edit supports undo and requires no application event handler. Checkbox toggles use MS Gothic when the document omits the state font.

The date picker shows a month grid with a Today button. Month and weekday names and the first day of the week follow the editor's locale, not the browser. Arrow keys move between days and across month edges. Home/End move to the week's edges. PageUp/PageDown move one month; Shift with either key moves one year. Month and year controls let you jump directly. Enter a regional numeric date or an ISO date in the text input, then press Enter or Apply. Invalid dates keep the popup open. The input requires a four-digit year. A day press writes the date immediately. Tab stays inside the popup; Escape returns focus to its opener. On short screens, the popup scrolls so all controls remain reachable.

Dropdowns support locale-aware typeahead, arrows, and Home/End. The selected entry is marked. Combo boxes also accept free text and use ArrowDown to enter the suggestion list. Moving focus does not change the document.

A control that Word saved with empty content opens showing its placeholder, the way Word shows the glossary block its w:placeholder names, or the type's default prompt when the document carries no glossary. The caret can enter the prompt, a press selects it whole, and the first keystroke replaces it. Typing at either edge of the prompt replaces it too, and the caret lands after the text. Typing at the end of a control's content stays inside the control, as in Word; ArrowRight leaves it. Inside a control, text typed at the end of a hyperlink leaves the link but stays in the control. Deleting everything a control holds brings its prompt back, so the control never becomes an invisible gap; Backspace and Delete at the edge of a text, date, or list control take one character, and only a content-locked control, a checkbox, or a picture goes as one unit. The saved file carries a shown prompt under w:showingPlcHdr, which is what Word writes once it has rendered the control.

A building block gallery control lists the blocks the document's glossary part stores for the control's gallery and category, sorted by category and name. A pick replaces the control's content with the block's body and gives its paragraphs fresh identities, as one undo step. A document with no matching block shows a note instead of an empty list.

A picture control's button opens a file dialog. The chosen image replaces the control's picture and keeps the drawing's size and metadata. The picture is selected first, so image commands address it. The engine accepts PNG, JPEG, GIF, BMP, and WebP files and refuses anything else with a reason in lastRejection.

The engine paints these menus with the docx-content-control-menu, docx-content-control-menu-item, and docx-content-control-calendar-* classes, and the control buttons with docx-content-control-widget. Style them with these classes and the --doc-* tokens. For a different UI, see Customize control popups.

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

Operational limits

Forms protection permits edits inside unlocked controls. Control locks and bindings still apply. Read-only and comments-only protection refuse control writes. See Document protection.

  • 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.
  • A building block gallery lists only blocks stored in the document's own glossary part. Blocks from Word's Building Blocks template are not available. A one-paragraph block fills an inline control; a block with several paragraphs or a table needs a block-level control. Picks support self-contained text, direct formatting, math, tables, and unbound nested controls. Blocks that reference styles, numbering, media, relationships, bookmarks, notes, fields, or revisions return unsupported; their glossary resources are not imported. This also applies when a style with the same name already exists in the main document.
  • Replacing a picture control's image is refused in suggesting mode, like every image replacement.
  • Control creation, filling, and removal do not create revisions in suggesting mode.

Customize control popups

Use popups.contentControl for the inspector. To customize value entry, choose:

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

Omit an entry to keep the engine's default behavior. In React and Vue, DocxEditorContentControlWidget provides composable parts and useContentControlWidget() exposes their draft state and actions. The root handles placement, keyboard navigation, dismissal, and focus return.

See Compose control popups for React and Vue examples, custom actions, and styling hooks.

Picture picks reject files larger than 32 MiB before reading them. A failed pick keeps a file input and an error message available for retry. Canceling the session, closing the editor, or entering suggesting mode during decoding prevents the write. Successful replacement clears placeholder state and removes a temporary control in the same undo step as the image change.

Replacement preserves the drawing's width, height, crop, name, and alt text. A source image with a different aspect ratio can stretch within that frame. Resize or crop the result as needed, and review the alt text when the new image depicts different content.

Next steps

On this page