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:
| Kind | Shape | For |
|---|---|---|
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>
);
}<script setup lang="ts">
import { useContentControl } from '@docx-editor.dev/vue';
const { control, setValue, canSetValue, setValueDisabledReason, remove, canRemove } =
useContentControl();
</script>
<template>
<div v-if="control">
<h4>{{ control.alias ?? control.tag ?? control.id }}</h4>
<p>{{ control.controlType }}</p>
<button
type="button"
:disabled="!canSetValue"
:title="setValueDisabledReason ?? undefined"
@click="setValue('Acme GmbH')"
>
Fill
</button>
<button type="button" :disabled="!canRemove" @click="remove()">Remove</button>
</div>
</template>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>
);
}<script setup lang="ts">
import { useDocxEditor } from '@docx-editor.dev/vue';
const editor = useDocxEditor();
function insertField() {
editor.value?.exec({
type: 'insertContentControl',
subtype: 'plainText',
tag: 'customerName',
title: 'Customer name',
});
}
</script>
<template>
<button type="button" @mousedown.prevent @click="insertField()">Insert field</button>
</template>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.
| Operation | Refusal condition | Reason |
|---|---|---|
| Fill or edit | The control or an ancestor locks its content | locked |
| Remove | The control or an ancestor locks its wrapper | locked |
| Fill | The control declares w:dataBinding | bound |
| Fill | The value does not match the control type | typeMismatch |
| Create | The selection crosses paragraphs | Inline controls cannot wrap block content |
| Create | The position is inside a hyperlink, field, or inline control | The 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:dataBindinground-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
- Editing API: the full object model and its batching rules
- Custom nodes: your own typed inline nodes over the same OOXML primitive
- React hooks and Vue composables