A document generated from a template and filled in by several people often contains dozens of content controls. Checking which fields have been changed and which controls still carry the template tags one by one through the properties panel is inefficient. If the alias, ID, tag and type of every control can be exported into a single table, the review work becomes much lighter. The same traversal can also be used in reverse: batch-modify the color and appearance of controls by type, so that fields with different purposes can be told apart at a glance in the document.
This article uses Spire.Doc for JavaScript to accomplish three tasks: read control properties and export a list, modify control color by type, and set display appearance by type. The document is loaded through a virtual file system in the browser, and the results are downloaded directly without going through a server.
This article covers three features:
- Read Content Control Properties
- Modify Content Control Color by Type
- Set Content Control Appearance by Type
Reading and modification rely on the same traversal logic, so the object types of the two kinds of controls should be clarified first:
| Object type | DocumentObjectType value |
Location |
|---|---|---|
| Inline content control | StructureDocumentTagInline |
In the ChildObjects of a paragraph |
| Block-level content control | StructureDocumentTag |
In the ChildObjects of a section body or a table cell |
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume that Spire.Doc is installed and the WebAssembly module is initialized.
Read Content Control Properties
The goal of reading is to summarize the key properties of every content control in the document into a text file. The example reads ContentControl.docx from public/data; place that file in the project's public/data directory before running it. Three storage locations need to be handled: inline controls in body paragraphs, inline controls in table cells, and block-level controls in the body. Only by traversing all three will the list be complete.
The properties that are read include:
| Property | Meaning |
|---|---|
SDTProperties.Alias |
The control alias, the name displayed in Word |
SDTProperties.Id |
The control ID, unique within the document |
SDTProperties.Tag |
The control tag, usually written in advance by the template |
SDTProperties.SDTType |
The control type, such as Text or DropDownList |
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the font, read
ContentControl.docxinto the VFS and load it as aDocument. - Traverse body paragraphs and table cells to collect inline controls, then collect block-level controls in the body.
- Read the alias, ID, tag and type of each control in turn and join them into tab-separated text.
- Write the text to the VFS and trigger a download to get a list file that can be imported into spreadsheet software.
The code for reading content control properties is as follows:
function App() {
const readContentControlProperties = async () => {
// Get the Spire.Doc WASM module
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the font and the document to be read
await window.spire.FetchFileToVFS('ContentControl.docx', '', `${process.env.PUBLIC_URL}/data/`);
const doc = new wasmModule.Document();
doc.LoadFromFile('ContentControl.docx');
// Collect inline controls and block-level controls
const found = [];
const blocks = [];
const scanParagraph = (paragraph) => {
for (let j = 0; j < paragraph.ChildObjects.Count; j++) {
const child = paragraph.ChildObjects.get_Item(j);
if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
found.push(child);
}
}
};
const scanTable = (table) => {
for (let r = 0; r < table.Rows.Count; r++) {
const row = table.Rows.get_Item(r);
for (let c = 0; c < row.Cells.Count; c++) {
const cell = row.Cells.get_Item(c);
for (let k = 0; k < cell.ChildObjects.Count; k++) {
const child = cell.ChildObjects.get_Item(k);
if (child.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) scanParagraph(child);
if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) found.push(child);
}
}
}
};
for (let s = 0; s < doc.Sections.Count; s++) {
const section = doc.Sections.get_Item(s);
for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
const obj = section.Body.ChildObjects.get_Item(i);
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) scanParagraph(obj);
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Table) scanTable(obj);
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) blocks.push(obj);
}
}
// Build the property list
let report = 'Alias\tID\tTag\tType\r\n';
for (const sdt of [...found, ...blocks]) {
const properties = sdt.SDTProperties;
const type = String(properties.SDTType).replace('SdtType.', '');
report += `${properties.Alias}\t${properties.Id}\t${properties.Tag}\t${type}\r\n`;
}
// Write to the VFS and download the text file
window.dotnetRuntime.Module.FS.writeFile('GetContentControlProperty.txt', report);
doc.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile('GetContentControlProperty.txt');
const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'GetContentControlProperty.txt';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Read Content Control Properties</h1>
<button onClick={readContentControlProperties}>Export Property List</button>
</div>
);
}
export default App;
The body and table of the sample document are shown below:

The exported list is as follows:
| Alias | ID | Tag | Type |
|---|---|---|---|
| Name | 2001 | employee-name | Text |
| Department | 2002 | department | DropDownList |
| Hire Date | 2003 | hire-date | DatePicker |
| Laptop | 2004 | laptop | CheckBox |
| External Monitor | 2005 | monitor | CheckBox |
| Onboarding Notice | 2006 | onboarding-notice | RichText |
How to Cover Tables and Block-Level Controls During Traversal
Paragraphs directly in the body cover only some of the controls. Controls in a table require entering Table.Rows and Row.Cells before scanning the ChildObjects of the cell; block-level controls appear directly in Body.ChildObjects, and their content is not inside a paragraph. The example handles the three cases separately and merges the results for output.
During traversal, check DocumentObjectType before accessing properties. Reading SDTProperties on an ordinary paragraph returns an empty value or raises an exception; filtering by type first avoids such problems.
Modify Content Control Color by Type
When a document contains text, rich text, date and other controls at the same time, telling purposes apart by color is more intuitive than reading aliases one by one. The example reads ModifySTDColor.docx from public/data; place that file in the project's public/data directory before running it. For example, mark all text controls green and all rich text controls orange. The color is written to SDTProperties.Color and assigned according to the control type.
Modifying the color involves the following members:
| Member | Purpose |
|---|---|
SDTProperties.SDTType |
Determines the control type and therefore which color to assign |
SDTProperties.Color |
The control color, constructed with Color.FromArgb() |
Color.FromArgb(a, r, g, b) |
Creates a color from ARGB components |
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the font, read
ModifySTDColor.docxinto the VFS and load it as aDocument. - Traverse paragraphs and block-level controls in the body and take out each content control object.
- Branch on
SDTType: assign green to text controls and orange to rich text controls. - Save the result file with
SaveToFile(), read it from the VFS and trigger a browser download.
The code for modifying content control color by type is as follows:
function App() {
const modifySDTColor = async () => {
// Get the Spire.Doc WASM module
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the font and the document to be processed
await window.spire.FetchFileToVFS('ModifySTDColor.docx', '', `${process.env.PUBLIC_URL}/data/`);
const doc = new wasmModule.Document();
doc.LoadFromFile('ModifySTDColor.docx');
// Iterate over the sections and modify control colors by type
for (let s = 0; s < doc.Sections.Count; s++) {
const section = doc.Sections.get_Item(s);
for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
const obj = section.Body.ChildObjects.get_Item(i);
const applyColor = (sdt) => {
switch (sdt.SDTProperties.SDTType) {
case wasmModule.SdtType.RichText:
sdt.SDTProperties.Color = wasmModule.Color.FromArgb(255, 237, 125, 49);
break;
case wasmModule.SdtType.Text:
sdt.SDTProperties.Color = wasmModule.Color.FromArgb(255, 0, 128, 0);
break;
}
};
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) {
applyColor(obj);
}
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) {
for (let j = 0; j < obj.ChildObjects.Count; j++) {
const child = obj.ChildObjects.get_Item(j);
if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
applyColor(child);
}
}
}
}
}
// Save the result file
const outputFileName = 'ModifySDTColor_out.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Modify Content Control Color by Type</h1>
<button onClick={modifySDTColor}>Generate Document</button>
</div>
);
}
export default App;
After the color is modified, the document looks like this: the tags of text controls are green and the tags of rich text controls are orange:

Control Color and Text Color Are Not the Same Thing
SDTProperties.Color sets the color of the control tag, not the color of the text inside the control. Text color belongs to TextRange.CharacterFormat.TextColor and has to be set for each text fragment. The two handle different parts: the former identifies the control itself, and the latter controls body typography.
The control color is visible only when Appearance is Tags. If the document uses the default appearance, the color will not show even after it is assigned; set SdtAppearance.Tags as well, or use the border appearance described later.
Set Content Control Appearance by Type
Appearance determines how content controls are presented in the Word editing view, and the example also reads ContentControl.docx from public/data. During the filling stage, field boundaries should be clear, so text, drop-down list and date controls can be set to the tag appearance; during the review stage, the layout should be clean, so check boxes can be set to the hidden appearance, keeping only the box glyphs. Setting them by type lets the same document serve different purposes.
Appearance is controlled by SDTProperties.Appearance; the common values are as follows:
| Value | Display effect | Suitable scenario |
|---|---|---|
SdtAppearance.Tags |
Shows alias tags on both sides of the content | Fill-in fields whose names need to be explicit |
SdtAppearance.BoundingBox |
Draws a border around the control range | Block-level content, a whole paragraph of explanation |
SdtAppearance.Hidden |
Hides the control frame | Controls that already have a visual mark, such as check boxes |
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the font, read
ContentControl.docxinto the VFS and load it as aDocument. - Traverse paragraphs and block-level controls in the body and take out each content control object.
- Branch on
SDTTypeto setAppearance: tags for text, drop-down list and date, border for rich text, hidden for check boxes. - Save the result file with
SaveToFile(), read it from the VFS and trigger a browser download.
The code for setting content control appearance by type is as follows:
function App() {
const setContentControlAppearance = async () => {
// Get the Spire.Doc WASM module
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the document to be processed
await window.spire.FetchFileToVFS('ContentControl.docx', '', `${process.env.PUBLIC_URL}/data/`);
const doc = new wasmModule.Document();
doc.LoadFromFile('ContentControl.docx');
// Iterate over the sections and set the display appearance by type
for (let s = 0; s < doc.Sections.Count; s++) {
const section = doc.Sections.get_Item(s);
for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
const obj = section.Body.ChildObjects.get_Item(i);
const applyAppearance = (sdt) => {
switch (sdt.SDTProperties.SDTType) {
case wasmModule.SdtType.Text:
case wasmModule.SdtType.DropDownList:
case wasmModule.SdtType.DatePicker:
sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.Tags;
break;
case wasmModule.SdtType.RichText:
sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.BoundingBox;
break;
case wasmModule.SdtType.CheckBox:
sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.Hidden;
break;
}
};
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) {
applyAppearance(obj);
}
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) {
for (let j = 0; j < obj.ChildObjects.Count; j++) {
const child = obj.ChildObjects.get_Item(j);
if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
applyAppearance(child);
}
}
}
}
}
// Save the result file
const outputFileName = 'SetContentControlAppearance_out.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Set Content Control Appearance by Type</h1>
<button onClick={setContentControlAppearance}>Generate Document</button>
</div>
);
}
export default App;
The document after setting the appearance is shown below:

Appearance Only Affects the Editing View
Appearance is a visual hint for editors and does not change the control type, content or lock state. After the appearance is set to Hidden, the control no longer shows a frame in Word, but it can still be selected and filled in; neither printing nor PDF export leaves a tag or border behind. When the filling behavior needs to be restricted, use lock settings instead of relying on appearance.
FAQ
Why is there no visible change after setting the control color
SDTProperties.Color applies to the control tag and is displayed only when Appearance is Tags. If the document still uses the default appearance, the color will not appear on the page. Set Appearance to SdtAppearance.Tags and save the document again, and the tag will carry the configured color.
How to tell whether a control is inline or block-level
Read DocumentObjectType: StructureDocumentTagInline is an inline control located inside a paragraph, while StructureDocumentTag is a block-level control located directly in the section body or a table cell. Both expose their properties through SDTProperties, but they have different traversal entry points, and a block-level control does not go through a paragraph.
Why are the content controls in a table not read
Table content is not in the ChildObjects of body paragraphs; you have to enter Table.Rows, then access Row.Cells, and finally scan Cell.ChildObjects. The scanTable helper in the example handles this layer specifically. If only body paragraphs are traversed, the fields in the table will be missed.
Why does a newly created control read back with a different type
The type is determined by the content of the saved document. When creating a control, set SDTType explicitly, then write the content and save; if the type is omitted or left to the default, some controls are recognized as RichText when read back. When reading an existing document, SDTType reflects the type actually stored in the document, which can be used to verify that the template was generated as expected.
Get a Free License
Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
