Spire.Doc for JavaScript (78)
Children categories
Setting a page background for a Word document is one of the most common ways to polish contracts, official papers and brand materials: a soft base color, or a background image that matches the corporate visual identity, is enough to give the whole document a consistent visual tone. Spire.Doc for JavaScript performs this setting directly in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.
This article covers three core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Set a Solid Color Background
The solid color background involves three stages: first, load the font files and the target Word file into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, set Background.Type to BackgroundType.Color, and assign a built-in color value to Background.Color; finally, save the document to the VFS with SaveToFile, read the generated docx file from it, wrap it as a Blob, and trigger a browser download.
function App() {
const SetSolidColorBackground = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into the virtual file system (VFS)
let inputFileName = "ScienceTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create Word document
let doc = new docModule.Document();
// Load the file
doc.LoadFromFile(inputFileName);
// Set the background type as Color
doc.Background.Type = docModule.BackgroundType.Color;
// Set the background color
doc.Background.Color = docModule.Color.get_LightYellow();
// Define the output file name
const outputFileName = "SetSolidColorBackground_out.docx";
// Save the document to the specified path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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 a Solid Color Background for a Word Document</h1>
<button onClick={SetSolidColorBackground}>Generate</button>
</div>
);
}
export default App;
After Background.Color has been applied, the whole page is filled with the built-in LightYellow background color.

Set a Gradient Background
A gradient background follows the same flow as a solid color background; only the middle stage differs. Set Background.Type to BackgroundType.Gradient, retrieve the background gradient object through Background.Gradient, set the start color Color1 and the end color Color2 separately, and then use ShadingStyle and ShadingVariant to control the direction of the gradient and the way it transitions.
function App() {
const SetGradientBackground = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into the virtual file system (VFS)
let inputFileName = "ScienceTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create Word document
let doc = new docModule.Document();
// Load the file
doc.LoadFromFile(inputFileName);
// Set the background type as Gradient
doc.Background.Type = docModule.BackgroundType.Gradient;
let gradient = doc.Background.Gradient;
// Set the start color and the end color of the gradient
gradient.Color1 = docModule.Color.get_White();
gradient.Color2 = docModule.Color.get_LightBlue();
// Set the shading style and variant of the gradient
gradient.ShadingVariant = docModule.GradientShadingVariant.ShadingDown;
gradient.ShadingStyle = docModule.GradientShadingStyle.Horizontal;
// Define the output file name
const outputFileName = "SetGradientBackground_out.docx";
// Save the document to the specified path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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 a Gradient Background for a Word Document</h1>
<button onClick={SetGradientBackground}>Generate</button>
</div>
);
}
export default App;
After Background.Gradient has been applied, the page background is filled with a white and light blue gradient.

Set a Picture Background
A picture background is similar to the two backgrounds above; the difference lies in how the resources are prepared and assigned. At the loading stage the background image has to be loaded into the VFS together with the font files and the target Word file; then set Background.Type to BackgroundType.Picture and call Background.SetPicture with the path of the image inside the VFS to tile the image across the whole page as the background.
function App() {
const SetImageBackground = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into the virtual file system (VFS)
let inputFileName1 = "ScienceTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName1, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the background image into the virtual file system (VFS)
let inputFileName2 = "Background.png";
await window.spire.FetchFileToVFS(inputFileName2, "", `${process.env.PUBLIC_URL}static/data/`);
// Load a Word document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName1);
// Set the background type as Picture
doc.Background.Type = docModule.BackgroundType.Picture;
// Set the background picture
doc.Background.SetPicture(inputFileName2);
// Define the output file name
const outputFileName = "SetImageBackground_out.docx";
// Save the document to the specified path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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 a Picture Background in a Word Document</h1>
<button onClick={SetImageBackground}>Generate</button>
</div>
);
}
export default App;
After Background.SetPicture has been applied, the image is tiled across the whole page as the background.

FAQ
The background does not appear in print
Cause: Word does not print page background colors or background pictures by default. This is a printing setting of the Word client, not a lost background setting in the document. The background displays normally while the document is open and is only ignored in the printed output.
Solution: To keep the background in a printed copy, select Print background colors and images under File > Options > Display in Word before printing. If the background has to be output in every environment, use a full-page shape in the header or a watermark to simulate it instead.
The picture background has no effect
Cause: Background.Type was not set to BackgroundType.Picture before SetPicture was called, or the background image was not loaded into the VFS via FetchFileToVFS, so SetPicture cannot find the image file.
Solution: Set the background type first, then pass the name of the image that has already been loaded into the VFS:
document.Background.Type = wasmModule.BackgroundType.Picture;
document.Background.SetPicture("Background.png");
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.
Protect Word Documents and Restrict Editing with JavaScript in React
2026-09-21 07:15:09 Written by Amy ZhaoIn document distribution and collaboration, controlling what a reader may do with a document often matters more than controlling who may open it — when a contract template goes to a client, the client should fill in the blank items without touching the agreed clauses; when a final draft goes to the team, comments should be allowed but the body text should not be edited directly. Requirements like these are met by restricting editing, which is a different concept from setting an open password. Spire.Doc for JavaScript processes Word documents directly in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts and file resources — no backend server required.
This article covers two core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Protect a Document with a Specified Protection Type
Word's editing restrictions come in five protection types, each covering a different editable scope: NoProtection (no restriction), AllowOnlyComments (comments only), AllowOnlyFormFields (form fields only), AllowOnlyReading (reading only), and AllowOnlyRevisions (tracked changes only). Calling the Protect method with a ProtectionType enum value and a password applies a restriction to the whole document as required.
Protecting a document with a specified protection type involves three stages: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, and call the Protect method to specify the protection type together with the password that lifts the restriction; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const protectWithSpecifiedType = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
const inputFileName = 'Template.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// Load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Protect the document with the "AllowOnlyReading" type; the password lifts the restriction
doc.Protect({ type: docModule.ProtectionType.AllowOnlyReading, password: "123456" });
// Define the output file name and save
const outputFileName = "SpecifiedProtectionType.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Protect a Document with a Specified Type</h1>
<button onClick={protectWithSpecifiedType}>Generate</button>
</div>
);
}
export default App;
Once the document is protected with the AllowOnlyReading type, its content can only be viewed and the editing commands on the ribbon are restricted.

Lock Only Specified Sections
Protecting the whole document uniformly is not always appropriate. In templates such as contracts and quotations, usually only a few places need to be filled in while every other clause has to stay locked. In that case, protect the whole document with AllowOnlyFormFields first, then release the section that may be edited through the ProtectForm property, which gives per-section control over the permissions.
Locking only specified sections involves three stages: first, load the font files into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, create several sections with AddSection and write content into them, call Protect to protect the whole document for form fields only, and set the ProtectForm property of the section to be released to false; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const lockSpecifiedSections = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create a new document and add two sections
const doc = new docModule.Document();
let s1 = doc.AddSection();
let s2 = doc.AddSection();
// Write content into each of the two sections
s1.AddParagraph().AppendText("Spire.Doc demo, section 1");
s2.AddParagraph().AppendText("Spire.Doc demo, section 2");
// Protect the whole document for form fields only
doc.Protect({ type: docModule.ProtectionType.AllowOnlyFormFields, password: "123" });
// Release section 2 on its own so that it can be edited
s2.ProtectForm = false;
// Define the output file name and save
const outputFileName = 'LockSpecifiedSections.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Lock Specified Sections of a Word Document</h1>
<button onClick={lockSpecifiedSections}>Generate</button>
</div>
);
}
export default App;
Once section 2 has been released, only section 1 keeps its editing restriction in the document.

FAQ
A section is still not editable after ProtectForm = false
Cause: ProtectForm only takes effect while the document is protected with AllowOnlyFormFields (form fields only). If the document uses another protection type such as AllowOnlyReading, releasing a single section has no effect.
Solution: Make sure the protection type passed to Protect matches the operation that releases the section:
doc.Protect({ type: wasmModule.ProtectionType.AllowOnlyFormFields, password: "123" });
s2.ProtectForm = false;
A protected document can still be selected and copied
Cause: Every protection type restricts editing behaviour, not reading behaviour. AllowOnlyReading only blocks changes to the body text; it does not affect selection, copying or searching. To restrict reading as well, an open password should be used rather than editing restrictions.
Solution: Choose the means for the actual purpose — use document encryption when the content must not be taken away, and use editing restrictions only when the content must not be changed:
doc.Protect({ type: wasmModule.ProtectionType.AllowOnlyReading, password: "123456" });
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.
Set and Remove Editable Ranges in Word Documents with JavaScript in React
2026-09-21 02:59:40 Written by Amy ZhaoIn documents such as contracts, forms, and official templates, you often want the person filling them in to modify only a few specific places — signature details, project name, acceptance conclusion — while every other clause has to stay exactly as it is. Setting an editable range on a document pins down "what may be changed" precisely and leaves everything else read-only. Spire.Doc for JavaScript does this entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts and file resources — no backend server required.
This article covers two core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Set an Editable Range
Setting an editable range involves three stages: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, call Protect to make the whole document read-only, and create a pair of PermissionStart and PermissionEnd markers that share the same id to mark the specified paragraph as an editable range; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const SetEditableRange = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the input document into VFS
const inputFileName = "SetEditableRange.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document object and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Protect the whole document: everything outside the editable range is read-only
doc.Protect({ type: docModule.ProtectionType.AllowOnlyReading, password: "password" });
// Create the permission markers: a start and an end with the same id form one editable range
const start = new docModule.PermissionStart(doc, "testID");
const end = new docModule.PermissionEnd(doc, "testID");
// Insert the markers into the first paragraph: the start at the beginning, the end appended at the end
doc.Sections.get_Item(0).Paragraphs.get_Item(0).ChildObjects.Insert(0, start);
doc.Sections.get_Item(0).Paragraphs.get_Item(0).ChildObjects.Add(end);
// Save the document
const outputFileName = "Set Editable Range.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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 Editable Range in a Word Document</h1>
<button onClick={SetEditableRange}>
Generate
</button>
</div>
);
}
export default App;
In the sample document, the places that need to be filled in are highlighted with light shading (a visual hint only — it has nothing to do with how the editable range is set). Once the editable range has been set, only the shaded paragraph can be modified, and the remaining clauses are read-only in Word.

Remove an Editable Range
Removing an editable range takes a single pass: walk each section and each paragraph of the document in turn, look for the PermissionStart and PermissionEnd objects in the paragraph's ChildObjects collection, and remove each one you find from the collection.
There is one detail that is easy to trip over: ChildObjects.Remove shrinks the collection immediately, so the indexes of the remaining elements all shift forward. The index must therefore not be incremented while removing, or every marker you delete causes the one right behind it to be skipped, leaving markers behind.
function App() {
const RemoveEditableRange = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the input document into VFS
const inputFileName = "RemoveEditableRange.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document object and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Iterate over every section and paragraph and delete the permission markers
for (let i = 0; i < doc.Sections.Count; i++) {
const section = doc.Sections.get_Item(i);
for (let j = 0; j < section.Body.Paragraphs.Count; j++) {
const paragraph = section.Body.Paragraphs.get_Item(j);
// Remove on a match; the collection shrinks, so the index is not incremented
for (let k = 0; k < paragraph.ChildObjects.Count;) {
const obj = paragraph.ChildObjects.get_Item(k);
if (obj instanceof docModule.PermissionStart || obj instanceof docModule.PermissionEnd) {
paragraph.ChildObjects.Remove(obj);
} else {
k++;
}
}
}
}
// Save the document
const outputFileName = "Remove Editable Range.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Remove Editable Ranges from a Word Document</h1>
<button onClick={RemoveEditableRange}>
Generate
</button>
</div>
);
}
export default App;
Removing the markers only affects how the editable area is divided; the text content and the formatting of the document do not change in any way.

FAQ
The editable range is set, but the content inside it still cannot be edited
Cause: The permission markers only take effect together with the document's editing restriction. If PermissionStart and PermissionEnd are inserted without calling Protect, the document never enters the protected state and the markers have no effect whatsoever; on top of that, the ids of the two markers must match exactly before Word recognizes them as one single editable range.
Solution: Enable the editing restriction first, and then create the paired markers with the same id:
// Enable protection first so that the markers mean something
document.Protect({ type: wasmModule.ProtectionType.AllowOnlyReading, password: "password" });
// The start and the end must use the same id
const start = new wasmModule.PermissionStart(document, "testID");
const end = new wasmModule.PermissionEnd(document, "testID");
Some markers are missed when removing editable ranges
Cause: ChildObjects.Remove shifts the indexes of all the subsequent elements in the collection forward. If the index is incremented while removing inside a for loop, every object that is removed causes the one right behind it to be skipped, and the more markers are left in the document, the more obvious the misses become.
Solution: Switch to "remove on a match, do not increment the index", or collect the objects to be removed first and then iterate backwards:
for (let k = 0; k < paragraph.ChildObjects.Count;) {
const obj = paragraph.ChildObjects.get_Item(k);
if (obj instanceof wasmModule.PermissionStart || obj instanceof wasmModule.PermissionEnd) {
paragraph.ChildObjects.Remove(obj);
// Do not increment k here: check the new object at the current index
} else {
k++;
}
}
The document is still read-only after the markers are removed
Cause: PermissionStart and PermissionEnd only mark "which areas may be edited"; removing them does not turn off the document's editing restriction. The protection is still in effect, so at that point the whole document cannot be edited.
Solution: If the protection is no longer needed, call Unprotect after removing the markers; if the document was protected with a password, pass the password that was used at the time:
document.Unprotect("password");
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.
Add, Delete, and Replace Images in Word with JavaScript in React
2026-09-21 02:34:17 Written by Amy ZhaoWorking with images in Word documents is one of the most common requirements in day-to-day office development. Whether you are adding a company logo to a contract template, cleaning up leftover placeholder pictures in a document, or batch-replacing the stamps and icons in a report, manipulating images dynamically keeps document content in sync with your business data. Spire.Doc for JavaScript handles Word image processing entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts, documents, and image resources — no backend server required.
This article covers three core features:
- Add an image to a Word document
- Delete images from a Word document
- Replace images in a Word document
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Add an Image to a Word Document
Adding an image involves three phases: first, load the font files, the target Word document, and the image to be inserted into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, get the target section and create a DocPicture object, call LoadImage to load the image and set its position, size, and text wrapping style, then use ChildObjects.Insert to insert the picture at the specified position in the specified paragraph; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const insertImage = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the input document into VFS
const inputFileName = "BlankTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the image file to be inserted into VFS
const inputImgFileName = "Word.png";
await window.spire.FetchFileToVFS(inputImgFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Get the first section
let section = doc.Sections.get_Item(0);
// Add a heading paragraph
let paragraph = section.AddParagraph();
paragraph.AppendText("The sample demonstrates how to insert an image into a document.");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
// Add a new paragraph to hold the picture
paragraph = section.AddParagraph();
paragraph.AppendText("This is a picture.");
// Create a picture object and load the image file
let picture = new docModule.DocPicture(doc);
picture.LoadImage({ imgFile: inputImgFileName });
// Set the position of the picture
picture.HorizontalPosition = 50.0;
picture.VerticalPosition = 60.0;
// Set the size of the picture
picture.Width = 200;
picture.Height = 200;
// Set the text wrapping style of the picture
picture.TextWrappingStyle = docModule.TextWrappingStyle.Through;
// Insert the picture at the beginning of the paragraph (index 0)
paragraph.ChildObjects.Insert(0, picture);
// Save the document
const outputFileName = "Insert Image.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Insert an Image into a Word Document</h1>
<button onClick={insertImage}>
Generate
</button>
</div>
);
}
export default App;
The code above loads the picture through DocPicture, sets its absolute position to (50, 60) and its size to 200×200, applies the Through text wrapping style, and finally inserts it at the beginning of the second paragraph

Delete Images from a Word Document
Deleting images also involves three phases: first, load the font files and the Word document to be processed into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, walk the sections, the paragraphs, and the child objects inside each paragraph level by level, and use DocumentObjectType.Picture to determine whether an object is a picture — if it is, call ChildObjects.Remove to remove it from the paragraph; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const RemoveImage = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the input document into VFS
const inputFileName = "ImageTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
let removedCount = 0;
// Iterate over every section and paragraph and delete the pictures in each paragraph
for (let i = 0; i < doc.Sections.Count; i++) {
let sec = doc.Sections.get_Item(i);
for (let j = 0; j < sec.Paragraphs.Count; j++) {
let para = sec.Paragraphs.get_Item(j);
// Collect all the picture objects in the paragraph
let pictures = [];
for (let k = 0; k < para.ChildObjects.Count; k++) {
let docObj = para.ChildObjects.get_Item(k);
if (docObj.DocumentObjectType == docModule.DocumentObjectType.Picture) {
pictures.push(docObj);
}
}
// Remove the pictures from the paragraph one by one
for (let m = 0; m < pictures.length; m++) {
para.ChildObjects.Remove(pictures[m]);
removedCount++;
}
}
}
// Save the document
const outputFileName = "Delete Image.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Delete Images from a Word Document</h1>
<button onClick={RemoveImage}>
Generate
</button>
</div>
);
}
export default App;
The code above walks the document structure and removes every picture object, while the text content and formatting in the paragraphs stay unchanged. The picture on the left is the document before deletion, and the one on the right is the same document after all the images have been removed

Replace Images in a Word Document
Replacing an image can be seen as a combination of deleting and adding. It involves three phases: first, load the font files, the target Word document, and the new image file into the WASM virtual file system via FetchFileToVFS; then walk the sections and paragraphs, locate the picture objects and record their index within the paragraph, their original size, and their text wrapping style, call ChildObjects.Remove to remove the old picture, and then create a new DocPicture and insert it at the original index with the same parameters; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const ReplaceImage = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the input document into VFS
const inputFileName = "ImageTemplate.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the new picture that will take the place of the old ones into VFS
const inputImgFileName = "NewLogo.png";
await window.spire.FetchFileToVFS(inputImgFileName, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Iterate over every section and paragraph and replace the pictures in each paragraph
for (let i = 0; i < doc.Sections.Count; i++) {
let sec = doc.Sections.get_Item(i);
for (let j = 0; j < sec.Paragraphs.Count; j++) {
let para = sec.Paragraphs.get_Item(j);
// Collect the picture objects in the paragraph along with their index in ChildObjects
let pictures = [];
for (let k = 0; k < para.ChildObjects.Count; k++) {
let docObj = para.ChildObjects.get_Item(k);
if (docObj.DocumentObjectType == docModule.DocumentObjectType.Picture) {
pictures.push({ index: k, picture: docObj });
}
}
// Iterate backwards so that removing an object does not shift the remaining indexes
for (let m = pictures.length - 1; m >= 0; m--) {
let index = pictures[m].index;
let picture = pictures[m].picture;
// Record the size and the text wrapping style of the original picture
let width = picture.Width;
let height = picture.Height;
let wrappingStyle = picture.TextWrappingStyle;
// Remove the original picture
para.ChildObjects.Remove(picture);
// Create a new picture that keeps the size and wrapping style of the original one
let newPicture = new docModule.DocPicture(doc);
newPicture.LoadImage({ imgFile: inputImgFileName });
newPicture.Width = width;
newPicture.Height = height;
newPicture.TextWrappingStyle = wrappingStyle;
// Insert the new picture at the original index
para.ChildObjects.Insert(index, newPicture);
}
}
}
// Save the document
const outputFileName = "Replace Image.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Replace Images in a Word Document</h1>
<button onClick={ReplaceImage}>
Generate
</button>
</div>
);
}
export default App;
The code above removes each original picture while inserting the new picture at the index the original one occupied, reusing its size and its wrapping style, so the layout of the document is exactly the same as before the replacement

FAQ
The inserted image does not show up, or the image file cannot be found
Cause: The image file has not been loaded into the WASM virtual file system, or the path passed to FetchFileToVFS does not match the actual location of the file, so LoadImage cannot read the image data from VFS.
Solution: Load the image into VFS with FetchFileToVFS before inserting it, and make sure the target file name matches the file name passed to LoadImage:
await window.spire.FetchFileToVFS(
'Word.png', '', `${process.env.PUBLIC_URL}static/data/`
);
let picture = new wasmModule.DocPicture(doc);
picture.LoadImage({ imgFile: 'Word.png' });
Images are left behind, or end up misplaced, after deletion or replacement
Cause: ChildObjects.Remove shifts the indexes of all subsequent child objects forward. If you read ChildObjects.Count while removing objects in the same loop, some of the pictures are skipped; and when replacing, inserting at the new index taken after the removal puts the picture at the wrong position.
Solution: Collect the picture objects and their original indexes in one pass first, then iterate backwards over that collection, and insert the new picture at the original index that was recorded:
// Collect the pictures and their indexes first
pictures.push({ index: k, picture: docObj });
// Then iterate backwards, removing each picture and inserting the new one back at its original index
for (let m = pictures.length - 1; m >= 0; m--) {
para.ChildObjects.Remove(pictures[m].picture);
para.ChildObjects.Insert(pictures[m].index, newPicture);
}
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.
Add, Delete, and Modify Shapes in Word with JavaScript in React
2026-09-21 02:33:48 Written by Amy ZhaoWorking with shapes in Word documents is one of the most common requirements in day-to-day office development. Whether you are stamping a rounded-rectangle signature box onto a contract template, annotating approval steps with a group of flowchart shapes, cleaning up leftover decorative shapes in a document, or restyling the shapes in an old template to match a new brand palette, manipulating shapes dynamically keeps document content in sync with your business data. Spire.Doc for JavaScript handles Word shapes entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts, documents, and image resources — no backend server required.
This article covers three core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Add Shapes to a Word Document
Adding shapes involves three phases: first, load the font files into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, add a section and a paragraph in turn, and call Paragraph.AppendShape to insert a shape of the specified type and size into the paragraph, anchoring it to the page with HorizontalOrigin/VerticalOrigin while setting its absolute coordinates through HorizontalPosition/VerticalPosition; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const appendShape = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create a new document
const doc = new docModule.Document();
// Add a section
let sec = doc.AddSection();
// Add a paragraph to hold the shapes
let paragraph = sec.AddParagraph();
let x = 60, y = 40, lineCount = 0;
for (let i = 1; i < 20; i++) {
if (lineCount > 0 && lineCount % 8 == 0) {
// Start a new page once 8 rows are filled, and reset the starting coordinates
paragraph.AppendBreak(docModule.BreakType.PageBreak);
x = 60;
y = 40;
lineCount = 0;
}
// Add a shape and set its size
let shape = paragraph.AppendShape(50, 50, docModule.ShapeType.fromValue(i));
// Position the shape in absolute coordinates relative to the page
shape.HorizontalOrigin = docModule.HorizontalOrigin.Page;
shape.HorizontalPosition = x;
shape.VerticalOrigin = docModule.VerticalOrigin.Page;
shape.VerticalPosition = y + 50;
// Calculate the coordinates of the next shape
x = x + shape.Width + 50;
if (i > 0 && i % 5 == 0) {
y = y + shape.Height + 120;
lineCount++;
x = 60;
}
}
// Save the document
const outputFileName = "Add Shapes.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Add Shapes to a Word Document</h1>
<button onClick={appendShape}>
Generate
</button>
</div>
);
}
export default App;
The code above inserts 19 different preset shapes in a loop, each 50×50, retrieving the shape type one by one with ShapeType.fromValue(i) and laying them out in rows and columns from the page origin — 5 shapes per row, and an automatic page break via AppendBreak once 8 rows are filled

When you need to insert several related shapes at once — building a flowchart out of rectangles, parallelograms, and arrows, for example — you can instead call Paragraph.AppendShapeGroup to create a shape group first, then add text boxes, arrows, and other child shapes to it with ChildObjects.Add so they are laid out together. Coordinates inside a shape group are relative to the group itself, so you need to work out the scale factor with Width / 1000.0 and Height / 1000.0, then divide the target coordinates of each child shape by that factor before assigning them to HorizontalPosition/VerticalPosition. A text box is created with new wasmModule.TextBox(doc) and its outline specified through SetShapeType; its Format.LineColor sets the stroke color in exactly the same way as StrokeColor does for a regular shape.
Delete Shapes from a Word Document
Deleting shapes involves three phases: first, load the font files and the Word document to be processed into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, walk the sections and paragraphs of the document level by level, and use DocumentObjectType to determine whether a child object in a paragraph is a shape — regular shapes and text boxes in a document both exist as type Shape, shape groups are ShapeGroup, while a text box created in memory with new wasmModule.TextBox(doc) and not yet saved is reported separately as TextBox, so all three types must be checked; once identified, collect the objects and remove them from the paragraph one by one with ChildObjects.Remove; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const removeShape = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
const inputFileName = 'ShapeTemplate.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
let removedCount = 0;
// Iterate over every section and paragraph and delete the shapes in each paragraph
for (let i = 0; i < doc.Sections.Count; i++) {
let sec = doc.Sections.get_Item(i);
for (let j = 0; j < sec.Paragraphs.Count; j++) {
let para = sec.Paragraphs.get_Item(j);
// Collect the shapes in the paragraph in one pass: regular shapes and text boxes are Shape, shape groups are ShapeGroup
let shapes = [];
for (let k = 0; k < para.ChildObjects.Count; k++) {
let docObj = para.ChildObjects.get_Item(k);
let objType = docObj.DocumentObjectType;
if (objType == docModule.DocumentObjectType.Shape
|| objType == docModule.DocumentObjectType.ShapeGroup
|| objType == docModule.DocumentObjectType.TextBox) {
shapes.push(docObj);
}
}
// Remove the shapes from the paragraph one by one
for (let m = 0; m < shapes.length; m++) {
para.ChildObjects.Remove(shapes[m]);
removedCount++;
}
}
}
// Save the document
const outputFileName = "Delete Shapes.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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>Delete Shapes from a Word Document</h1>
<button onClick={removeShape}>
Generate
</button>
</div>
);
}
export default App;
The code above walks the document structure and removes every shape on the page, while the text content and formatting in the paragraphs stay unchanged

Modify Shapes in a Word Document
Modifying shapes involves three phases: first, load the font files and the Word document to be processed into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the file, walk the sections and paragraphs to locate the shape objects within them, and for regular shapes and text boxes set FillColor and StrokeColor directly to change the colors and set Rotation, Width, and Height to adjust the rotation angle and size, while for a shape group you drill down into its ChildObjects and modify each child shape in turn; finally, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const modifyShape = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
const inputFileName = 'ShapeTemplate.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Iterate over every section and paragraph and modify the shapes in each paragraph
for (let i = 0; i < doc.Sections.Count; i++) {
let sec = doc.Sections.get_Item(i);
for (let j = 0; j < sec.Paragraphs.Count; j++) {
let para = sec.Paragraphs.get_Item(j);
for (let k = 0; k < para.ChildObjects.Count; k++) {
let docObj = para.ChildObjects.get_Item(k);
let objType = docObj.DocumentObjectType;
// Modify the fill color, outline color, rotation, and size of regular shapes and text boxes
if (objType == docModule.DocumentObjectType.Shape) {
docObj.FillColor = docModule.Color.get_Orange();
docObj.StrokeColor = docModule.Color.get_Red();
docObj.Rotation = 15;
docObj.Width = docObj.Width * 1.2;
docObj.Height = docObj.Height * 1.2;
}
// Modify a shape group: drill into the group and change the outline color of each child shape
if (objType == docModule.DocumentObjectType.ShapeGroup) {
for (let n = 0; n < docObj.ChildObjects.Count; n++) {
let child = docObj.ChildObjects.get_Item(n);
child.StrokeColor = docModule.Color.get_Purple();
}
}
}
}
}
// Save the document
const outputFileName = "Modify Shapes.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx });
// Release resources
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { 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 Existing Shapes in a Word Document</h1>
<button onClick={modifyShape}>
Generate
</button>
</div>
);
}
export default App;
The code above keeps each shape's original position and text while restyling every shape in the document with an orange fill and a red outline, scaling it up to 120% and rotating it 15 degrees, and the child shapes inside shape groups get the new outline color as well

FAQ
Shapes end up misplaced or pushed off the page after being added
Cause: The meaning of the coordinates in HorizontalPosition/VerticalPosition depends on the frame of reference (the origin). If HorizontalOrigin/VerticalOrigin are not set explicitly, the coordinates default to the paragraph or the column, so they shift along with paragraph indentation and page margins, and the shape drifts away from the intended position.
Solution: Specify the frame of reference with HorizontalOrigin/VerticalOrigin first, and then set the coordinate values:
// Position the shape relative to the page
shape.HorizontalOrigin = wasmModule.HorizontalOrigin.Page;
shape.HorizontalPosition = x;
shape.VerticalOrigin = wasmModule.VerticalOrigin.Page;
shape.VerticalPosition = y + 50;
Some objects are missed when deleting shapes
Cause: In Spire.Doc a shape does not have just one DocumentObjectType: regular shapes and text boxes are uniformly Shape, shape groups are ShapeGroup, and a text box created in memory and not yet saved is TextBox. Checking only for Shape will miss shape groups and unsaved text boxes. On top of that, ChildObjects.Remove shifts the indexes of all subsequent child objects forward, so reading ChildObjects.Count while removing during the same loop will also skip some objects.
Solution: Collect all three types into an array in one pass first, then process the array one item at a time; when modifying the child shapes inside a shape group, drill down into its ChildObjects and iterate separately:
// Collect the shape objects of all three types first
if (objType == wasmModule.DocumentObjectType.Shape
|| objType == wasmModule.DocumentObjectType.ShapeGroup
|| objType == wasmModule.DocumentObjectType.TextBox) {
shapes.push(docObj);
}
// Then remove them together
for (let m = 0; m < shapes.length; m++) {
para.ChildObjects.Remove(shapes[m]);
}
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.
Converting a Word document to HTML preserves the original paragraph structure, styles, and images while rendering directly in the browser, which makes it widely useful for online preview, content publishing, and full-text search. Spire.Doc for JavaScript performs this conversion entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.
This article covers two core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Convert Word to HTML
Converting Word to HTML involves three stages: first, load the font file and the target Word file into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the file, use HtmlExportOptions to specify that both CSS and images are output in embedded form, and call SaveToFile to save the document as HTML; finally, read the generated HTML file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const wordToHtml = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Check if the module is ready
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load fonts and the Word file into VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const inputFileName = 'ToHtml.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Load the Word document
const wordDocument = new docModule.Document();
wordDocument.LoadFromFile(inputFileName);
// Embed the CSS styles into the HTML and embed images as Base64
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.Internal;
wordDocument.HtmlExportOptions.ImageEmbedded = true;
// Convert the document to HTML
const outputFileName = 'ToHtml-result.html';
wordDocument.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Html });
// Read the converted file from VFS and trigger download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = window.document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
// Release resources
wordDocument.Dispose();
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert Word To HTML</h1>
<button onClick={wordToHtml}>
Generate
</button>
</div>
);
}
export default App;
HTML page generated from a Word document via SaveToFile

Convert Word to HTML with export options
The output in the previous section is a single HTML file with CSS and images embedded in it. When a document is large, or when you want to maintain styles centrally and reuse image resources, you usually need to export CSS and images as separate files. HtmlExportOptions provides the corresponding settings, allowing HTML, style sheets, and images to be output separately.
The conversion flow is similar to the previous section, except that the result is a directory: you need to create the directory in VFS first, then use properties such as CssStyleSheetFileName and ImagesPath to specify where each type of resource is stored. Once conversion is complete, read that directory recursively, package everything into a zip, and download it in one go.
import JSZip from 'jszip';
function App() {
const wordToHtmlWithOptions = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Check if the module is ready
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load fonts and the Word file into VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const inputFileName = 'ToHtml.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Create the output directory in VFS
const outputDirectoryName = 'ToHTMLFolder/';
window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// Load the Word document
const wordDocument = new docModule.Document();
wordDocument.LoadFromFile(inputFileName);
// Export the CSS styles to a separate file
wordDocument.HtmlExportOptions.CssStyleSheetFileName = outputDirectoryName + 'sample.css';
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.External;
// Export images to a separate directory
wordDocument.HtmlExportOptions.ImageEmbedded = false;
wordDocument.HtmlExportOptions.ImagesPath = outputDirectoryName + 'Demo/';
// Export form fields as plain text
wordDocument.HtmlExportOptions.IsTextInputFormFieldAsText = true;
// Convert the document to HTML
const outputFileName = 'ToHtmlExportOption-out.html';
wordDocument.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Html });
// Release resources
wordDocument.Dispose();
// Read the output directory recursively and write each level of files into the zip
const zip = new JSZip();
const addFilesToZip = async (folderPath, zipFolder) => {
let items = await window.dotnetRuntime.Module.FS.readdir(folderPath);
items = items.filter((item) => item !== '.' && item !== '..');
for (const item of items) {
const itemPath = `${folderPath}/${item}`;
try {
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zipFolder.file(item, fileData);
} catch (error) {
const zipSubFolder = zipFolder.folder(item);
await addFilesToZip(itemPath, zipSubFolder);
}
}
};
// Package the HTML file together with the resource directory
zip.file(outputFileName, window.dotnetRuntime.Module.FS.readFile(outputFileName));
await addFilesToZip(outputDirectoryName, zip);
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(zipBlob);
// Trigger download
const a = window.document.createElement('a');
a.href = url;
a.download = 'ToHTMLFolder.zip';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert Word To HTML With Export Options</h1>
<button onClick={wordToHtmlWithOptions}>
Generate
</button>
</div>
);
}
export default App;
HTML, CSS, and image files generated after configuring the export options

Note that Spire.Doc does not write images directly into the directory pointed to by ImagesPath. Instead, it creates an external_images subdirectory underneath it to hold the images. As a result, the output directory typically forms a hierarchy such as Demo/external_images/*.png, which must be read level by level — this is why addFilesToZip is implemented recursively in the example above.
FAQ
Fonts in the exported HTML do not match the original document
Cause: The font files are missing from the WASM virtual file system. Spire.Doc reads fonts from VFS during conversion to perform layout calculations and font name resolution. If the fonts are not preloaded, the fonts used in the original document are replaced with substitute fonts, and the font-family in the exported CSS will not match the original. If the original document uses a symbol font such as Wingdings, the corresponding characters will also appear garbled.
Solution: Load the font files into VFS via FetchFileToVFS before conversion. For Chinese, Japanese, and Korean documents, use a font with broad coverage such as ARIALUNI.TTF:
await window.spire.FetchFileToVFS(
'ARIALUNI.TTF', '/Library/Fonts/', '/'
);
Exported HTML loses its styles and images when opened
Cause: In external mode (CssStyleSheetType.External combined with ImageEmbedded = false), CSS and images are output as separate files to the specified directory, and the HTML keeps only relative path references. If you download the HTML file on its own, the browser cannot find the corresponding style sheet and images, and the page degrades into unstyled plain text.
Solution: Package the HTML file together with the resource directory and download them as a whole, so that the relative path references remain valid (see the addFilesToZip example above). If you do not need separate resource files, you can switch to embedded mode instead:
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.Internal;
wordDocument.HtmlExportOptions.ImageEmbedded = true;
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.
Converting a Word document to images is the most common approach for online preview, thumbnail generation, and preventing content from being copied at will — the resulting images keep a consistent layout on any device. Spire.Doc for JavaScript performs this conversion directly in the browser via WebAssembly, managing input and output files through a virtual file system (VFS) — no backend server required.
This article covers two core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Page to Image
Converting a document page to an image involves three stages: first, load the font file and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, call SaveImageToStreams with a pageIndex to render the specified page as an image, and save it to VFS; finally, read the generated image file from VFS, wrap it as a Blob, and create a download link.
import React from 'react';
function App() {
const ToImage = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the font file into the virtual file system (VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ToImage.docx';
// Load the target Word document into VFS
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Define the output file name
const outputFileName = "ToImage-result.png";
// Convert the first page to an image stream and save it to VFS
let img = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
img.Save(outputFileName);
// Release resources
doc.Dispose();
// Read the generated file from VFS and wrap it as a Blob
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'image/png'});
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>Convert a Specified Page to an Image</h1>
<button onClick={ToImage}>
Generate
</button>
</div>
);
}
export default App;
PNG image generated from a document page via SaveImageToStreams

Document Object to Image
Besides whole-page conversion, real projects often need to export a single element of a document as an image — for example, generating a preview image for a table, or extracting a shape from a document as standalone material. Paragraphs, tables, table rows, table cells, and shapes can all be copied into a newly created Document via the Clone method, and then rendered into an image with SaveImageToStreams. The conversion results are written uniformly to an output directory in VFS, and finally packaged into a single ZIP file with JSZip for the user to download.
Note that a shape cannot be added directly to a paragraph of a newly created document. The example first saves the document to a memory stream and then reloads it, so that the shape obtains a complete layout context in the new document before being rendered.
import React from 'react';
import JSZip from "jszip";
function App() {
const ToImage = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the font file into the virtual file system (VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the target Word document into VFS
const inputFileName = "ConvertObjectToImage.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Define and create the output directory in VFS
const outputDirectoryName = "outputFolder/";
await window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Get the first section and its body
let section = doc.Sections.get_Item(0);
let body = section.Body;
// Get the first paragraph and convert it to an image
let paragraph = body.Paragraphs.get_Item(0);
let imageStream1 = ConvertParagraphToImage(paragraph, docModule);
let imageFile1 = outputDirectoryName + "ConvertParagraphToImage.png";
window.dotnetRuntime.Module.FS.writeFile(imageFile1, imageStream1.Save());
// Get the first table and convert it to an image
let table = body.Tables.get_Item(0);
let imageStream2 = ConvertTableToImage(table, docModule);
let imageFile2 = outputDirectoryName + "ConvertTableToImage.jpg";
window.dotnetRuntime.Module.FS.writeFile(imageFile2, imageStream2.Save());
// Get the first row of the first table and convert it to an image
let row = table.Rows.get_Item(0);
let imageStream3 = ConvertTableRowToImage(row, docModule);
let imageFile3 = outputDirectoryName + "ConvertTableRowToImage.bmp";
window.dotnetRuntime.Module.FS.writeFile(imageFile3, imageStream3.Save());
// Get the first cell of the first row and convert it to an image
let cell = row.Cells.get_Item(0);
let imageStream4 = ConvertTableCellToImage(cell, docModule);
let imageFile4 = outputDirectoryName + "ConvertTableCellToImage.png";
window.dotnetRuntime.Module.FS.writeFile(imageFile4, imageStream4.Save());
// Iterate over the paragraphs and convert the shapes in them to images
for (let i = 0; i < section.Paragraphs.Count; i++) {
let para = section.Body.Paragraphs.get_Item(i);
for (let j = 0; j < para.ChildObjects.Count; j++) {
let docObj = para.ChildObjects.get_Item(j);
if (docObj.DocumentObjectType == docModule.DocumentObjectType.Shape) {
let imageStream5 = ConvertShapeToImage(docObj, docModule);
let imageFile5 = outputDirectoryName + "ConvertShapeToImage-" + j + ".png";
window.dotnetRuntime.Module.FS.writeFile(imageFile5, imageStream5.Save());
i++;
}
}
}
// Release resources
doc.Dispose();
// Package all images in the output directory into a ZIP file
const zip = new JSZip();
const addFilesToZip = async (folderPath, zipFolder) => {
let items = await window.dotnetRuntime.Module.FS.readdir(folderPath);
items = items.filter((item) => item !== "." && item !== "..");
for (const item of items) {
const itemPath = `${folderPath}/${item}`;
try {
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zipFolder.file(item, fileData);
} catch (error) {
const zipSubFolder = zipFolder.folder(item);
await addFilesToZip(itemPath, zipSubFolder);
}
}
};
await addFilesToZip(outputDirectoryName, zip);
const zipBlob = await zip.generateAsync({ type: "blob" });
// Read the generated file from VFS and wrap it as a Blob
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = "ConvertObjectToImage_out.zip";
a.click();
URL.revokeObjectURL(url);
};
// Convert a paragraph to an image
function ConvertParagraphToImage(paragraph, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.Body.ChildObjects.Add(paragraph.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// Convert a table to an image
function ConvertTableToImage(table, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.Body.ChildObjects.Add(table.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// Convert a table row to an image
function ConvertTableRowToImage(tableRow, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
let table = section.AddTable();
table.Rows.Add(tableRow.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// Convert a table cell to an image
function ConvertTableCellToImage(tableCell, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
let table = section.AddTable();
table.AddRow().Cells.Add(tableCell.Clone());
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
doc.Close();
return imageStream;
}
// Convert a shape to an image
function ConvertShapeToImage(shape, docModule) {
let doc = new docModule.Document();
let section = doc.AddSection();
section.AddParagraph().ChildObjects.Add(shape.Clone());
let memoryStream = new docModule.Stream();
doc.SaveToStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
doc.LoadFromStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
memoryStream.Close();
doc.Close();
return imageStream;
}
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert Elements to Images</h1>
<button onClick={ToImage}>
Generate
</button>
</div>
);
}
export default App;
Images inside the ZIP file generated after converting the document objects

FAQ
Missing or garbled text in the generated image
Cause: The font files required for rendering are missing from the WASM virtual file system. SaveImageToStreams reads fonts from VFS when rendering text — if they are not preloaded, text areas will be left blank or appear garbled.
Solution: Load the font files into VFS via FetchFileToVFS before conversion:
await window.spire.FetchFileToVFS(
'ARIALUNI.TTF', '/Library/Fonts/',
`${process.env.PUBLIC_URL}/static/font/`
);
Only the first page is generated
Cause: Each call to SaveImageToStreams renders only the single page specified by pageIndex. The example always passes 0, so a multi-page document only outputs an image of the first page.
Solution: Get the total page count via PageCount and iterate page by page, generating a separate image file for each page:
for (let i = 0; i < doc.PageCount; i++) {
let img = doc.SaveImageToStreams({
pageIndex: i, type: wasmModule.ImageType.Bitmap
});
img.Save(`ToImage-page-${i + 1}.png`);
}
Get a Free License
If you wish to remove the evaluation message from the resulting document, or to eliminate functional limitations, please contact our sales team to request a 30-day temporary license.
Add a Table of Contents to an Existing Word Document with JavaScript in React
2026-09-18 06:15:33 Written by Amy ZhaoA more common situation in real-world work is a document that is already written and has a complete chapter structure, but no table of contents was generated at the time. There is no need to rearrange the content — adding a TOC field on top of the existing heading styles is enough to produce a complete table of contents with page numbers and hyperlinks. The table of contents is generated and saved entirely in the browser, so files such as contracts and proposals never have to be uploaded to a server, which removes one step from the handling process.
This article takes a "2026 Annual Product and Technology Report" as an example and introduces four ways to add a table of contents to an existing document. The sample is based on Spire.Doc for JavaScript. The library runs in the browser as a WebAssembly module, and reads and writes fonts and document files through a virtual file system (VFS).
Compared with creating a new document, adding a table of contents to an existing document involves two extra key steps: loading the original document from VFS with LoadFromFile, and specifying the position of the TOC paragraph with Paragraphs.Insert. AddParagraph always appends a new paragraph to the end of its section, so inserting the TOC field directly on it makes the table of contents appear at the end of the document, which is the most common problem with existing documents.
This article covers the following topics:
- Add a Table of Contents at the Start of the Document
- Add a Table of Contents at a Specified Position
- Add a Custom Table of Contents
- Update the Page Numbers of an Existing Table of Contents
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, and that the sample document AddTocToExisting.docx is placed in the public/data/ directory of the project.
Add a Table of Contents at the Start of the Document
To add a table of contents at the start of the document, first write the TOC field into a new paragraph with AppendTOC, then move that paragraph to the very beginning of the document with Paragraphs.Insert(0, tocPara). AppendTOC takes a start level and an end level, and the table of contents collects only the headings that fall within that range:
| Usage | Scope |
|---|---|
AppendTOC(1, 3) |
Heading 1 to Heading 3 |
AppendTOC(1, 2) |
Heading 1 to Heading 2 |
AppendTOC(2, 3) |
Heading 2 to Heading 3 |
The example passes (1, 3), so the table of contents collects four chapters, ten second-level headings and two third-level headings, 16 entries in total. A page break is appended to the end of the TOC paragraph, so the existing content moves back by one page and the table of contents takes page 1 on its own.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttc, the bold fontmsyhbd.ttcand the sample documentAddTocToExisting.docxinto the virtual file system (VFS). - Create a
Documentobject, load the existing document withLoadFromFile, and get the first section. - Create a new paragraph, call
AppendTOC(1, 3)to write the TOC field, and append a page break withAppendBreak. - Move the TOC paragraph to the very beginning of the document with
Paragraphs.Insert(0, tocPara), and insert a "Table of Contents" heading paragraph above it. - Call
UpdateTableOfContentsto update the entries and page numbers, save the document withSaveToFile, and read the result from VFS to trigger the download.
The code for adding a table of contents at the start of the document is as follows:
function App() {
const addTocAtStart = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts and the sample document into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'AddTocToExisting.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// Create a document instance and load the existing document
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// Get the first section of the document
const section = doc.Sections.get_Item(0);
// Create a new paragraph and insert the TOC field, collecting Heading 1 through Heading 3 entries
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
// The TOC takes a page of its own, so the existing content starts on a new page
tocPara.AppendBreak(docModule.BreakType.PageBreak);
// Move the TOC paragraph to the very beginning of the document
section.Paragraphs.Insert(0, tocPara);
// TOC page header: the TOC entries carry no heading line, so a "Table of Contents" line is added here
const titleFormat = new docModule.CharacterFormat(doc);
titleFormat.FontName = 'Microsoft YaHei';
titleFormat.FontSize = 18;
titleFormat.Bold = true;
titleFormat.TextColor = docModule.Color.FromArgb(31, 40, 70);
const titlePara = section.AddParagraph();
titlePara.AppendText('Table of Contents').ApplyCharacterFormat(titleFormat);
titlePara.Format.AfterSpacing = 14;
section.Paragraphs.Insert(0, titlePara);
// Update the TOC to fill in entries and page numbers
doc.UpdateTableOfContents();
// Define the output file name and save
const outputFileName = 'Add a TOC at the Start of the Document.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the generated file from 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>Add a Table of Contents at the Start of the Document</h1>
<button onClick={addTocAtStart}>
Generate
</button>
</div>
);
}
export default App;
The table of contents is placed at the very beginning of the document and takes a page of its own, while the original cover, body text and footer page numbering stay unchanged. Because the table of contents occupies page 1, Chapter 1 of the body moves to page 3, and the page numbers in the table of contents are refreshed accordingly:
Table of contents page after adding a table of contents at the start of the document

Add a Table of Contents at a Specified Position
To add a table of contents at a specified position, only one thing changes: the index passed to Paragraphs.Insert is no longer 0 but the target position. The index is calculated in the order of the child objects in the body, and every paragraph and table takes one position, so the index in the paragraph collection cannot be used directly to find the insertion point. The example first walks Body.ChildObjects, finds the paragraph that has the Heading 1 style applied, and passes that object position to Paragraphs.Insert, so the table of contents lands before Chapter 1: the cover stays on page 1 and the table of contents starts on page 2.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttc, the bold fontmsyhbd.ttcand the sample documentAddTocToExisting.docxinto the virtual file system (VFS). - Create a
Documentobject, load the existing document, and get the first section. - Walk the
Body.ChildObjectsof the first section, find the position of the first Heading 1 paragraph, and use it as the insertion index for the table of contents. - Create a new paragraph, call
AppendTOC(1, 3)to write the TOC field and append a page break, insert it before Chapter 1 withParagraphs.Insert(index, tocPara), and add the "Table of Contents" heading paragraph. - Call
UpdateTableOfContentsto update the entries and page numbers, save the document, and read the result from VFS to trigger the download.
The code for adding a table of contents at a specified position is as follows:
function App() {
const addTocAtPosition = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts and the sample document into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'AddTocToExisting.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// Create a document instance and load the existing document
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// Get the first section of the document
const section = doc.Sections.get_Item(0);
// Locate the Chapter 1 heading among the body objects; tables also take a position, so paragraph-collection indexes cannot be used directly
let insertIndex = 0;
const children = section.Body.ChildObjects;
for (let i = 0; i < children.Count; i++) {
const obj = children.get_Item(i);
if (obj.DocumentObjectType === docModule.DocumentObjectType.Paragraph && obj.StyleName === 'Heading1') {
insertIndex = i;
break;
}
}
// Create a new paragraph and insert the TOC field, collecting Heading 1 through Heading 3 entries
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
tocPara.AppendBreak(docModule.BreakType.PageBreak);
// Insert the TOC before Chapter 1: cover → TOC → body
section.Paragraphs.Insert(insertIndex, tocPara);
// TOC page header
const titleFormat = new docModule.CharacterFormat(doc);
titleFormat.FontName = 'Microsoft YaHei';
titleFormat.FontSize = 18;
titleFormat.Bold = true;
titleFormat.TextColor = docModule.Color.FromArgb(31, 40, 70);
const titlePara = section.AddParagraph();
titlePara.AppendText('Table of Contents').ApplyCharacterFormat(titleFormat);
titlePara.Format.AfterSpacing = 14;
section.Paragraphs.Insert(insertIndex, titlePara);
// Update the TOC to fill in entries and page numbers
doc.UpdateTableOfContents();
// Define the output file name and save
const outputFileName = 'Add a TOC at a Specified Position.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the generated file from 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>Add a Table of Contents at a Specified Position</h1>
<button onClick={addTocAtPosition}>
Generate
</button>
</div>
);
}
export default App;
The table of contents lands between the cover and the body text: the cover is still page 1, the table of contents starts on page 2, and the body starts on page 3:
Table of contents inserted after the cover

Add a Custom Table of Contents
To add a custom table of contents, construct a TableOfContent object instead and pass a field switch string to its constructor. The difference from AppendTOC lies in how it is inserted: the TOC object has to be added to a paragraph manually, the field separator and field end marks have to be completed, and the object then has to be assigned to Document.TOC. The commonly used field switches are as follows:
| Switch | Description |
|---|---|
\o "1-2" |
Collects entries by built-in heading styles; here it means collecting Heading 1 to Heading 2 |
\h |
Writes hyperlinks for the TOC entries, so that a click jumps to the corresponding chapter |
\z |
Hides page numbers and tab leaders in Web Layout view |
\u |
Collects entries by the outline level of the paragraphs |
The example uses \o "1-2" \h \z \u, so the table of contents collects only four chapters and ten second-level headings, 14 entries in total, and third-level headings such as 2.2.1 and 2.2.2 no longer appear.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttc, the bold fontmsyhbd.ttcand the sample documentAddTocToExisting.docxinto the virtual file system (VFS). - Create a
Documentobject, load the existing document, and get the first section. - Construct the TOC object with
new TableOfContent(doc, '{\\o "1-2" \\h \\z \\u}'), add it to a new paragraph, and complete the field separator and field end marks. - Assign the TOC object to
doc.TOC, insert it at the very beginning of the document withParagraphs.Insert(0, tocPara), and add the "Table of Contents" heading paragraph. - Call
UpdateTableOfContentsto update the entries and page numbers, save the document, and read the result from VFS to trigger the download.
The code for adding a custom table of contents is as follows:
function App() {
const addCustomToc = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts and the sample document into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'AddTocToExisting.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// Create a document instance and load the existing document
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// Get the first section of the document
const section = doc.Sections.get_Item(0);
// Build a TOC object with custom field switches: collect Heading 1 to Heading 2, with hyperlinked entries
const toc = new docModule.TableOfContent(doc, '{\\o "1-2" \\h \\z \\u}');
const tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
// Complete the field separator and field end marks
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText('TOC');
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
tocPara.AppendBreak(docModule.BreakType.PageBreak);
// Bind this TOC to the document
doc.TOC = toc;
// Move the TOC paragraph to the very beginning of the document
section.Paragraphs.Insert(0, tocPara);
// TOC page header
const titleFormat = new docModule.CharacterFormat(doc);
titleFormat.FontName = 'Microsoft YaHei';
titleFormat.FontSize = 18;
titleFormat.Bold = true;
titleFormat.TextColor = docModule.Color.FromArgb(31, 40, 70);
const titlePara = section.AddParagraph();
titlePara.AppendText('Table of Contents').ApplyCharacterFormat(titleFormat);
titlePara.Format.AfterSpacing = 14;
section.Paragraphs.Insert(0, titlePara);
// Update the TOC to fill in entries and page numbers
doc.UpdateTableOfContents();
// Define the output file name and save
const outputFileName = 'Add a Custom TOC.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the generated file from 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>Add a Custom Table of Contents</h1>
<button onClick={addCustomToc}>
Generate
</button>
</div>
);
}
export default App;
The TOC entries stop at second-level headings, and the original third-level headings no longer enter the table of contents:
Two-level table of contents generated by the custom table of contents

Limit the Collected Levels with Field Switches
The field switch string determines the behavior of the table of contents, and adjusting the level range after \o changes the collection scope: change it to \o "1-3" and third-level headings enter the table of contents again; remove \h and the entries no longer carry hyperlinks, so the table of contents becomes a plain text page reference:
// Collect third-level headings and no longer write hyperlinks
const toc = new docModule.TableOfContent(doc, '{\\o "1-3" \\z \\u}');
Update the Page Numbers of an Existing Table of Contents
To update the page numbers of an existing table of contents, call UpdateTableOfContents before saving. The TOC entries and page numbers are the result of a field calculation, and they are written according to the layout at the moment the field is updated, so the TOC object does not have to be rebuilt. The example loads a document that already has a table of contents (the result of the first feature), appends Chapter 5 to the end of the body text, and then updates the table of contents.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the fonts and the sample document
WithTOC.docx, which already has a table of contents, into the virtual file system (VFS). - Create a
Documentobject, load the document, and get the first section. - Append a page break, a Chapter 5 heading and a paragraph of body text to the end of the body: apply the Heading 1 style to the heading, and keep the font and size of the original document for the body text.
- Call
UpdateTableOfContentsto update the table of contents, so that the entry and page number of the new chapter are written as well. - Save the result with
SaveToFile, and read the file from VFS to trigger the download.
The code for updating the page numbers of an existing table of contents is as follows:
function App() {
const updateToc = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts and the sample document that already has a TOC into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'WithTOC.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
// Create a document instance and load the existing document
const doc = new docModule.Document();
doc.LoadFromFile({ fileName: inputFileName });
// Get the first section of the document
const section = doc.Sections.get_Item(0);
// Append a chapter at the end of the body: page break first, then the heading and the text
const breakPara = section.AddParagraph();
breakPara.AppendBreak(docModule.BreakType.PageBreak);
const headingPara = section.AddParagraph();
headingPara.AppendText('Chapter 5 Follow-up Plan');
headingPara.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
// The appended text keeps the font of the original document
const bodyFormat = new docModule.CharacterFormat(doc);
bodyFormat.FontName = 'Microsoft YaHei';
bodyFormat.FontSize = 10.5;
const bodyPara = section.AddParagraph();
bodyPara.AppendText('The 2027 plan brings template management and permission auditing into the platform and opens a batch-fill API for business systems to call directly.').ApplyCharacterFormat(bodyFormat);
// Update the TOC so the new chapter's entry and page number are written
doc.UpdateTableOfContents();
// Define the output file name and save
const outputFileName = 'Update TOC Page Numbers.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the generated file from 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>Update the Page Numbers of an Existing Table of Contents</h1>
<button onClick={updateToc}>
Generate
</button>
</div>
);
}
export default App;
After the page numbers are updated, a "Chapter 5 Follow-up Plan" entry is added at the end of the table of contents, its page number points to page 9, and the original entries stay unchanged:
Chapter 5 appended to the end of the table of contents after its page numbers are updated

FAQ
Why does the table of contents I add appear at the end of the document
Cause: AddParagraph appends the new paragraph to the end of its section, so calling AppendTOC on it or adding a TableOfContent object to it naturally places the table of contents at the end. The content of an existing document has already been laid out, and its position does not move forward by itself.
Solution: Create the TOC paragraph first, then move it to the target position with Paragraphs.Insert:
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
section.Paragraphs.Insert(0, tocPara);
Why are the body headings missing from the table of contents
Cause: The table of contents collects entries by heading style or outline level. If the chapter headings in the original document were only made bold manually or given a larger font size, without built-in heading styles such as Heading 1 to Heading 3 applied, no collectable paragraph can be found after updating, and the table of contents may be empty or only report "No table of contents entries found".
Solution: After loading the document, call ApplyStyle on the paragraphs that should appear in the table of contents to apply a built-in heading style, and then update the table of contents:
const headingPara = section.Paragraphs.get_Item(2);
headingPara.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
Why is the position of the table of contents a few paragraphs off after specifying an index with Paragraphs.Insert
Cause: The index of Paragraphs.Insert is calculated in the order of the child objects in the body, and every paragraph and table takes one position. The more tables a document contains, the further the position of the same paragraph drifts between the paragraph collection and the body objects, so using the index of the paragraph collection directly is often a few paragraphs off.
Solution: Walk Body.ChildObjects to find the target paragraph and use its object position as the index:
let insertIndex = 0;
const children = section.Body.ChildObjects;
for (let i = 0; i < children.Count; i++) {
const obj = children.get_Item(i);
if (obj.DocumentObjectType === docModule.DocumentObjectType.Paragraph && obj.StyleName === 'Heading1') {
insertIndex = i;
break;
}
}
The page numbers in the table of contents do not match the actual pages
Cause: The page numbers of the table of contents are written from the layout at the moment the field is updated, and they are not synchronized automatically if content or headings are added or removed afterwards. In addition, inserting a table of contents at the start of the document moves the existing content back as a whole, so the page numbers change accordingly, which is an expected result.
Solution: Call UpdateTableOfContents again before saving, so that the entries and page numbers are recalculated from the latest content:
doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
Updating a custom table of contents throws ArgumentOutOfRange, or shows only the word TOC
Cause: After a TableOfContent object is added to a paragraph, if the field separator, the field end mark, or the assignment to Document.TOC is missing, the TOC field is incomplete: without the marks the page may show only placeholder text, and without the binding, updating is likely to throw an index-out-of-range exception.
Solution: The following three steps have to be completed together before the update is called:
const toc = new docModule.TableOfContent(doc, '{\\o "1-2" \\h \\z \\u}');
const tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText('TOC');
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
doc.TOC = toc;
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.
Add a Table of Contents to a New Word Document with JavaScript in React
2026-09-18 06:01:11 Written by Amy ZhaoA table of contents is a standard part of a long document. Its entries follow the heading hierarchy and carry page numbers, so readers can locate a section quickly; when content is added or removed, the entries and page numbers can also be updated together before saving, without checking page by page.
This article takes a "2026 Annual Product and Technology Report" as an example: it creates a Word document from scratch — cover, table-of-contents page, four chapters of body text and data tables — and then adds a table of contents to it. The sample is based on Spire.Doc for JavaScript. The library runs in the browser as a WebAssembly module, reads and writes fonts and document files through a virtual file system (VFS), and requires no backend service, so files never leave the user's device.
This article covers the following topics:
- Build the Sample Document
- Add a Default Table of Contents
- Set the Heading Levels Collected by the Table of Contents
- Add a Custom Table of Contents
- Modify the Table of Contents Entry Styles
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. So that each feature can be run directly, every code sample below is a complete single-file program: the builder for the sample document and the calling code are merged into the same block, which can replace the contents of App.js and run as it is.
Build the Sample Document
The code that builds the sample document is kept in report.js: the cover, the table-of-contents page heading, the four chapters of body text and the footer are all generated by this module, and an insertToc callback is reserved on the table-of-contents page, so that the four features that follow decide how the TOC field is written. Each section below inlines this builder as well, so report.js does not have to be created separately.
The sample document has four chapters and sixteen headings, spanning three levels from Heading 1 to Heading 3, and contains three data tables and one security notice block. The full heading hierarchy makes it easy to observe how the table of contents collects headings at different levels; the layout uses a consistent font, color scheme and line spacing, and a page break separates the table-of-contents page from the body text.
The specific steps are as follows:
- Create a
Documentand aSection, and set the A4 page margins, the footer distance and the footer content. - Write the cover: brand line, document-category block, title, subtitle and key figures row.
- Write the table-of-contents page heading and note, and call the
insertToccallback so that each feature can decide how the TOC field is inserted. - Write the four chapters of body text, applying the built-in Heading 1, Heading 2 and Heading 3 styles to chapter, section and subsection headings respectively.
- After the body text is written, call
UpdateTableOfContentsto update the table of contents, and return the completedDocumentobject.
The code that builds the sample document is as follows:
// Sample document builder: cover → TOC → four chapters
// The caller decides how the TOC is written through the insertToc callback; the body content and layout are shared by the four features
const FONT = 'Microsoft YaHei';
const NAVY = [31, 40, 70];
const TEAL = [28, 122, 94];
const GREY = [95, 105, 120];
const LIGHT = [240, 243, 247];
const ALT = [248, 250, 252];
const RULE = [210, 216, 224];
const WHITE = [255, 255, 255];
const CONTENT = 439; // A4 page width 595 pt, minus 78 pt on each side
// Layout helpers: a consistent font, color, line spacing and paragraph spacing
function createLayout(docModule, doc, section) {
const color = (rgb) => docModule.Color.FromArgb(rgb[0], rgb[1], rgb[2]);
const lines = (n) => n * 12; // Line spacing is measured in points; 12 pt is single spacing
const format = (size, rgb, bold = false, spacing = 0) => {
const cf = new docModule.CharacterFormat(doc);
cf.FontName = FONT;
cf.FontSize = size;
cf.TextColor = color(rgb);
cf.Bold = bold;
cf.CharacterSpacing = spacing;
return cf;
};
const paragraph = (content, o = {}) => {
const p = section.AddParagraph();
if (content) p.AppendText(content).ApplyCharacterFormat(format(o.size || 10.5, o.color || GREY, o.bold, o.spacing));
p.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p.Format.LineSpacing = lines(o.ls || 1.55);
p.Format.AfterSpacing = o.after === undefined ? 9 : o.after;
p.Format.BeforeSpacing = o.before || 0;
if (o.indent) p.Format.LeftIndent = o.indent;
if (o.align) p.Format.HorizontalAlignment = o.align;
if (o.rule) {
p.Format.Borders.Bottom.BorderType = docModule.BorderStyle.Single;
p.Format.Borders.Bottom.LineWidth = o.ruleWidth || 0.75;
p.Format.Borders.Bottom.Color = color(o.rule);
p.Format.Borders.Bottom.Space = o.ruleSpace === undefined ? 4 : o.ruleSpace;
}
return p;
};
// Blank line with an exact height, used for spacing on the cover
const spacer = (height) => {
const p = section.AddParagraph();
p.Format.LineSpacingRule = docModule.LineSpacingRule.Exactly;
p.Format.LineSpacing = height;
p.Format.BeforeSpacing = 0;
p.Format.AfterSpacing = 0;
return p;
};
// Apply a built-in style to heading paragraphs; the TOC collects entries by style
const heading = (content, level, o = {}) => {
const p = section.AddParagraph();
const size = level === 1 ? 16 : level === 2 ? 12.5 : 11;
p.AppendText(content).ApplyCharacterFormat(format(size, level === 2 ? TEAL : NAVY, true, level === 1 ? 0.6 : 0));
p.ApplyStyle({ builtinStyle: [null, docModule.BuiltinStyle.Heading1, docModule.BuiltinStyle.Heading2, docModule.BuiltinStyle.Heading3][level] });
p.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p.Format.LineSpacing = lines(1.25);
p.Format.BeforeSpacing = o.before === undefined ? (level === 1 ? 21 : 13) : o.before;
p.Format.AfterSpacing = level === 1 ? 9 : 4;
p.Format.KeepFollow = true;
if (level === 1) {
p.Format.Borders.Bottom.BorderType = docModule.BorderStyle.Single;
p.Format.Borders.Bottom.LineWidth = 1.5;
p.Format.Borders.Bottom.Color = color(TEAL);
p.Format.Borders.Bottom.Space = 5;
}
return p;
};
const table = (header, rows, widths) => {
const t = section.AddTable(true);
t.ResetCells(rows.length + 1, header.length);
t.SetBorders(docModule.BorderStyle.Hairline, 0.5, color(RULE));
t.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
widths.forEach((w, i) => t.SetColumnWidth(i, CONTENT * w / 100, docModule.WidthType.Twip));
const fill = (row, i, value, bg, textColor, bold, size) => {
const c = row.Cells.get_Item(i);
const p = c.AddParagraph();
p.AppendText(value).ApplyCharacterFormat(format(size, textColor, bold));
p.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p.Format.LineSpacing = lines(1.2);
p.Format.AfterSpacing = 0;
c.CellFormat.BackColor = color(bg);
c.CellFormat.Paddings.Left = 10;
c.CellFormat.Paddings.Right = 10;
c.CellFormat.Paddings.Top = 7;
c.CellFormat.Paddings.Bottom = 7;
c.CellFormat.VerticalAlignment = docModule.VerticalAlignment.Middle;
};
header.forEach((h, i) => fill(t.Rows.get_Item(0), i, h, NAVY, WHITE, true, 9));
rows.forEach((r, ri) => r.forEach((v, i) => fill(t.Rows.get_Item(ri + 1), i, v, ri % 2 ? ALT : WHITE, i === 0 ? NAVY : GREY, i === 0, 9.5)));
for (let i = 0; i < t.Rows.Count; i++) {
const row = t.Rows.get_Item(i);
row.RowFormat.IsBreakAcrossPages = false;
if (i === 0) row.IsHeader = true;
}
return t;
};
// Notice block: light cell plus a color bar on the left; no heading style, so it stays out of the TOC
const callout = (title, lines_) => {
const t = section.AddTable(false);
t.ResetCells(1, 1);
t.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
t.SetColumnWidth(0, CONTENT, docModule.WidthType.Twip);
const c = t.Rows.get_Item(0).Cells.get_Item(0);
c.CellFormat.BackColor = color(LIGHT);
c.CellFormat.Paddings.Left = 15;
c.CellFormat.Paddings.Right = 15;
c.CellFormat.Paddings.Top = 12;
c.CellFormat.Paddings.Bottom = 12;
c.CellFormat.Borders.Left.BorderType = docModule.BorderStyle.Single;
c.CellFormat.Borders.Left.LineWidth = 2.5;
c.CellFormat.Borders.Left.Color = color(TEAL);
const head = c.AddParagraph();
head.AppendText(title).ApplyCharacterFormat(format(9.5, NAVY, true));
head.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
head.Format.LineSpacing = lines(1.4);
head.Format.AfterSpacing = 3;
lines_.forEach((line) => {
const p = c.AddParagraph();
p.AppendText(line).ApplyCharacterFormat(format(9, GREY));
p.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p.Format.LineSpacing = lines(1.5);
p.Format.AfterSpacing = 2;
});
// The single-cell notice must not split across pages, so the color block is never cut
t.Rows.get_Item(0).RowFormat.IsBreakAcrossPages = false;
};
return { format, paragraph, spacer, heading, table, callout, color, lines };
}
// Footer: note text on the left, page number on the right
function addFooter(docModule, section, L) {
const footer = section.HeadersFooters.Footer.AddParagraph();
footer.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
footer.Format.LineSpacing = 12;
footer.Format.AfterSpacing = 0;
footer.Format.Borders.Top.BorderType = docModule.BorderStyle.Single;
footer.Format.Borders.Top.LineWidth = 0.5;
footer.Format.Borders.Top.Color = L.color(RULE);
footer.Format.Borders.Top.Space = 6;
footer.Format.Tabs.AddTab(CONTENT, docModule.TabJustification.Right, docModule.TabLeader.NoLeader);
footer.AppendText('NORTHSTAR TECHNOLOGY · PRODUCT & ENGINEERING · INTERNAL').ApplyCharacterFormat(L.format(8, GREY));
footer.AppendText('\t').ApplyCharacterFormat(L.format(8, GREY));
footer.AppendText('Page ').ApplyCharacterFormat(L.format(8, GREY));
footer.AppendField('PAGE', docModule.FieldType.FieldPage);
}
// Cover
function addCover(docModule, section, L) {
const { paragraph, spacer, format, color } = L;
paragraph('NORTHSTAR TECHNOLOGY · PRODUCT & ENGINEERING', { size: 8.5, color: TEAL, bold: true, spacing: 1.8, after: 6, rule: NAVY, ruleWidth: 1.5, ruleSpace: 8 });
// Color block plus document category, the visual anchor of the cover
const badge = section.AddTable(false);
badge.ResetCells(1, 1);
badge.Format.HorizontalAlignment = docModule.HorizontalAlignment.Left;
badge.SetColumnWidth(0, 120, docModule.WidthType.Twip);
const badgeCell = badge.Rows.get_Item(0).Cells.get_Item(0);
badgeCell.CellFormat.BackColor = color(TEAL);
badgeCell.CellFormat.Paddings.Left = 8;
badgeCell.CellFormat.Paddings.Right = 8;
badgeCell.CellFormat.Paddings.Top = 3;
badgeCell.CellFormat.Paddings.Bottom = 3;
const badgePara = badgeCell.AddParagraph();
badgePara.AppendText('ANNUAL TECHNICAL DOCUMENT').ApplyCharacterFormat(format(8, WHITE, true, 1.2));
badgePara.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
badgePara.Format.LineSpacing = 13;
badgePara.Format.AfterSpacing = 0;
badgePara.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
spacer(34);
paragraph('2026 Annual Product and Technology Report', { size: 30, color: NAVY, bold: true, after: 8, ls: 1.1 });
paragraph('Capability building, rollout progress and acceptance plan for the online document processing platform', { size: 12.5, color: GREY, after: 22, ls: 1.4 });
// Key figures row
const stat = section.AddTable(false);
stat.ResetCells(1, 3);
[['5', 'Capability Domains'], ['12', 'Key Features'], ['98%', 'Pass Rate']].forEach(([value, label], i) => {
const c = stat.Rows.get_Item(0).Cells.get_Item(i);
const p1 = c.AddParagraph();
p1.AppendText(value).ApplyCharacterFormat(format(18, TEAL, true));
p1.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p1.Format.LineSpacing = 22;
p1.Format.AfterSpacing = 3;
const p2 = c.AddParagraph();
p2.AppendText(label).ApplyCharacterFormat(format(9, GREY));
p2.Format.LineSpacingRule = docModule.LineSpacingRule.Multiple;
p2.Format.LineSpacing = 14;
p2.Format.AfterSpacing = 0;
c.CellFormat.BackColor = color(LIGHT);
c.CellFormat.Paddings.Left = 15;
c.CellFormat.Paddings.Right = 12;
c.CellFormat.Paddings.Top = 14;
c.CellFormat.Paddings.Bottom = 14;
c.CellFormat.VerticalAlignment = docModule.VerticalAlignment.Middle;
});
spacer(96);
paragraph('Document No. NS-TR-2026-018 Version V2.1 (Review Draft)', { size: 9, color: GREY, after: 5, rule: RULE });
paragraph('Prepared by Product & Engineering Published October 2026', { size: 9, color: GREY, after: 0 });
}
// Body outline: the first item is the heading level, and the level decides whether the entry is collected
const OUTLINE = [
[1, 'Chapter 1 Project Overview', [
'The online document processing platform was started in 2025, with the goal of letting business departments generate and circulate contracts, reports and statements without installing desktop software. Work in 2026 concentrates on three main lines: closing capability gaps, delivery cadence and quality assurance.',
]],
[2, '1.1 Background', [
'Business departments need to generate about 42,000 documents in fixed formats every month. This used to depend on scattered desktop tools and manual checking, at about 6 minutes per document. After the platform moved this work to the browser, generating a single document takes less than 9 seconds.',
'Documents no longer pass through a server either: both the source files and the result files stay on the user\'s device, which removes one step from the handling of sensitive content such as contracts and payroll.',
]],
[2, '1.2 Delivery Scope for This Phase', [
'The delivery scope for this year covers four areas: document creation, content editing, format conversion and batch processing. The modules and their status are listed in the table below.',
], ['Module', 'Deliverables', 'Status'], [
['Document creation', 'Paragraphs, tables, images, headers and footers', 'Delivered'],
['Content editing', 'Styles, tables of contents, bookmarks, find and replace', 'Delivered'],
['Format conversion', 'PDF, images, HTML, Markdown', 'Delivered'],
['Batch processing', 'Multi-file queues and template filling', 'In integration testing'],
], [26, 50, 24], 'Delivery in this phase assumes that processing is completed in the browser; only two kinds of data, templates and permissions, remain on the server.'],
[2, '1.3 Document Structure Conventions', [
'The report follows a uniform structure of three chapters plus an appendix: chapters are Heading 1, sections are Heading 2, and items that need further breakdown are Heading 3. The chapter number and the heading text are separated by two spaces, so that entries stay aligned at each level of indentation in the table of contents.',
'Every data table in the body text carries a header row, and the header repeats automatically when a table breaks across pages; column widths are allocated in proportion to the content rather than fixed, so that the same document stays readable at different page sizes.',
]],
[1, 'Chapter 2 Technical Architecture', [
'The platform separates the front end from the back end: the browser parses and renders documents, while the server handles storage, permissions and auditing. The interface between the two sides is bounded by the document object model, which avoids transferring files back and forth between the browser and the server.',
]],
[2, '2.1 Overall Architecture', [
'The document processing core is loaded as a WebAssembly module, and static resources such as fonts and templates are loaded on demand through a virtual file system. The application layer only calls the interfaces of the document object model and does not deal with memory management or thread scheduling underneath.',
]],
[2, '2.2 Key Modules', [
'The key modules are divided by responsibility into three groups: parsing, rendering and security. Parsing and rendering are two independent processing stages and can be replaced separately.',
]],
[3, '2.2.1 Document Parsing Engine', [
'The parsing engine reads the document structure and restores paragraphs, tables, images and field codes into an object tree in memory. Parsing does not modify the source file, so the same document can be read repeatedly by multiple workflows.',
]],
[3, '2.2.2 Rendering and Export', [
'The rendering module computes the layout from the page size and margins, and the export module writes the result in the target format. Both share the same pagination rules, so the export matches the page count of the on-screen preview.',
]],
[2, '2.3 Data and Security', [
'Documents are processed in the browser and source files never pass through the server; result files that need to be retained are uploaded by the user explicitly, and access permissions are set along two dimensions, department and project.',
], null, null, null, null, ['Security Notice', [
'Document parsing and rendering are both completed inside the user\'s browser; the platform does not keep source files on the server.',
'If a result file needs to be archived, the user should upload it explicitly and set an access period per project.',
]]],
[1, 'Chapter 3 Implementation Plan', [
'Implementation proceeds in four phases, with a phase review at the end of each phase; work moves on to the next phase once the review is passed.',
]],
[2, '3.1 Milestones', [
'The start and end dates and the deliverables of each phase are listed in the table below; dates are given in calendar months.',
], ['Phase', 'Timeline', 'Deliverables'], [
['Design', 'February 2026', 'Architecture specification, interface list'],
['Core development', 'March–June 2026', 'Parsing and rendering modules'],
['Integration testing', 'July–August 2026', 'End-to-end demo environment'],
['Acceptance and launch', 'September–October 2026', 'Acceptance report, operations manual'],
], [24, 32, 44]],
[2, '3.2 Risks and Mitigation', [
'The main risk identified so far is pagination drift caused by missing font resources. The mitigation is to ship the font files with the application and validate the font list at startup.',
], ['Risk', 'Impact', 'Mitigation'], [
['Missing fonts', 'Page breaks shift', 'Ship fonts with the bundle and validate the list'],
['Very long paragraphs', 'Longer generation time', 'Split paragraphs by chapter and process in segments'],
['Browser differences', 'Preview and export disagree', 'Use the export result as the acceptance baseline'],
], [24, 32, 44]],
[1, 'Chapter 4 Acceptance Criteria', [
'Acceptance is based on two items, functional completeness and performance metrics; if either is not met, the work enters a remediation process.',
]],
[2, '4.1 Functional Acceptance', [
'Functional acceptance covers 12 key features, executed one by one against the acceptance script; the pass rate is currently 98%. The two failures both concern progress feedback in batch processing.',
]],
[2, '4.2 Performance Metrics', [
'Performance is measured in three areas: generation time, batch processing and memory usage. The target and measured values are listed in the table below.',
], ['Metric', 'Target', 'Measured'], [
['Generate one 30-page document', '15 seconds or less', '9.4 seconds'],
['Process 20 documents in batch', '5 minutes or less', '3 minutes 12 seconds'],
['Peak memory usage', '600 MB or less', '468 MB'],
], [40, 32, 28]],
];
function addBody(section, L) {
const { paragraph, heading, table, callout } = L;
OUTLINE.forEach(([level, title, texts, header, rows, widths, tail, calloutData]) => {
heading(title, level);
texts.forEach((content) => paragraph(content, { after: header ? 10 : 9 }));
if (header) table(header, rows, widths);
if (tail) paragraph(tail, { size: 9, after: 0, before: 10 });
if (calloutData) callout(calloutData[0], calloutData[1]);
});
paragraph('The metrics above are summarized quarterly; the retest results for the fourth quarter will be updated in the next report.', { size: 9.5, after: 0, before: 10 });
}
// Heading bar and note on the table-of-contents page
function addTocHeading(L) {
L.paragraph('CONTENTS', { size: 8, color: TEAL, bold: true, spacing: 2.4, after: 4, ls: 1.1 });
L.paragraph('Table of Contents', { size: 19, color: NAVY, bold: true, spacing: 1.6, after: 8, ls: 1.1, rule: TEAL, ruleWidth: 1, ruleSpace: 8 });
L.paragraph('The entries below are generated automatically from the heading styles applied in the document, and the page numbers refresh whenever the body text changes.', { size: 9, after: 14 });
}
function addPageBreak(docModule, section) {
const p = section.AddParagraph();
p.AppendBreak(docModule.BreakType.PageBreak);
p.Format.LineSpacingRule = docModule.LineSpacingRule.Exactly;
p.Format.LineSpacing = 1;
p.Format.AfterSpacing = 0;
}
// Notice block below the TOC: no heading style, so it does not enter the TOC
const TOC_NOTES = [
'· TOC entries are generated from the paragraphs that have a heading style applied; the scope depends on the heading levels specified when the TOC is inserted.',
'· Page numbers come from the layout computed at save time, so the TOC has to be updated again after content is added to or removed from the body.',
'· The cover and the table-of-contents page are counted in the page numbering, so Chapter 1 of the body starts on page 3.',
];
// Build the whole document; insertToc(doc, section) inserts the TOC field on the contents page
export function buildReport(docModule, insertToc) {
const doc = new docModule.Document();
const section = doc.AddSection();
section.PageSetup.Margins.Left = 78;
section.PageSetup.Margins.Right = 78;
section.PageSetup.Margins.Top = 72;
section.PageSetup.Margins.Bottom = 70;
section.PageSetup.FooterDistance = 36;
const L = createLayout(docModule, doc, section);
addFooter(docModule, section, L);
addCover(docModule, section, L);
// Page break after the cover so the contents page stands on its own
addPageBreak(docModule, section);
addTocHeading(L);
// Each feature decides how the TOC field is written
insertToc(doc, section);
// Notice block below the TOC
L.spacer(16);
L.callout('Reading Notes', TOC_NOTES);
// Page break after the TOC so the body starts on a new page
addPageBreak(docModule, section);
addBody(section, L);
// Update the TOC to fill in entries and page numbers
doc.UpdateTableOfContents();
return doc;
}
// Read the generated file from VFS and trigger a browser download
export function download(fileName) {
const fileArray = window.dotnetRuntime.Module.FS.readFile(fileName);
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 = fileName;
a.click();
URL.revokeObjectURL(url);
}
Add a Default Table of Contents
To add a default table of contents, insert the TOC field on the contents page with the AppendTOC method. The method takes a start level and an end level, and the table of contents collects only the headings that fall within that range:
| Usage | Scope |
|---|---|
AppendTOC(1, 3) |
Heading 1 to Heading 3 |
AppendTOC(1, 2) |
Heading 1 to Heading 2 |
AppendTOC(2, 3) |
Heading 2 to Heading 3 |
The example passes (1, 3), so the table of contents collects headings at three levels — Chapter 1, 1.1 and 2.2.1 — and the page numbers are connected to the entries by a tab leader.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttcand the bold fontmsyhbd.ttcinto the virtual file system (VFS). - Call
buildReportto build the sample document, and insert the default TOC field on the contents page withAppendTOC(1, 3). - Save the document with the
SaveToFilemethod under the file name "Create a Default TOC in Word Document.docx". - Call the
downloadmethod to read the result file from VFS and trigger the browser download.
The code for adding a default table of contents is as follows:
function App() {
const addDefaultToc = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// Build the sample document and insert the default TOC field on the contents page
const report = buildReport(docModule, (doc, section) => {
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
});
// Save the result file
const outputFileName = 'Create a Default TOC in Word Document.docx';
report.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
report.Dispose();
// Read the generated file from VFS and trigger the download
download(outputFileName);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Add a Default Table of Contents to a New Document</h1>
<button onClick={addDefaultToc}>
Generate
</button>
</div>
);
}
export default App;
Once the table of contents has been updated, the contents page holds entries that collect three heading levels, with page numbers and tab leaders:
Default table of contents generated with AppendTOC(1, 3)

Set the Heading Levels Collected by the Table of Contents
To set the heading levels collected by the table of contents, adjust the two parameters of AppendTOC. With the end level changed from 3 to 2, the table of contents keeps only Heading 1 and Heading 2 entries, and third-level headings such as 2.2.1 and 2.2.2 no longer appear; with the start level changed to 2, Heading 1 entries are left out in the same way.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttcand the bold fontmsyhbd.ttcinto the virtual file system (VFS). - Call
buildReportto build the sample document, and insert a TOC field withAppendTOC(1, 2)on the contents page so that only two heading levels are collected. - Save the document with the
SaveToFilemethod under the file name "Create a Two-Level TOC in Word Document.docx". - Call the
downloadmethod to read the result file from VFS and trigger the browser download.
The code for setting the collection levels is as follows:
function App() {
const addTwoLevelToc = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// Start level 1, end level 2: collect only Heading 1 and Heading 2 entries
const report = buildReport(docModule, (doc, section) => {
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 2);
});
// Save the result file
const outputFileName = 'Create a Two-Level TOC in Word Document.docx';
report.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
report.Dispose();
// Read the generated file from VFS and trigger the download
download(outputFileName);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Add a Two-Level Table of Contents to a New Document</h1>
<button onClick={addTwoLevelToc}>
Generate
</button>
</div>
);
}
export default App;
With the collection levels set, the entries in the table of contents stop at Heading 2, and 2.2.1 and 2.2.2 no longer appear:
Two-level table of contents generated with AppendTOC(1, 2)

Add a Custom Table of Contents
To add a custom table of contents, construct a TableOfContent object instead and pass a field switch string to its constructor. The difference from AppendTOC lies in how it is inserted: the TOC object has to be added to a paragraph manually, the field separator and field end marks have to be completed, and the object then has to be assigned to Document.TOC.
The commonly used field switches are as follows:
| Switch | Description |
|---|---|
\o "1-3" |
Collects entries by built-in heading styles; here it means collecting Heading 1 to Heading 3 |
\h |
Writes hyperlinks for the TOC entries, so that a click jumps to the corresponding chapter |
\z |
Hides page numbers and tab leaders in Web Layout view |
\u |
Collects entries by the outline level of the paragraphs |
Changing the switch string to \o "1-2" makes the table of contents collect only Heading 1 and Heading 2 entries; removing \h means hyperlinks are no longer written for the entries. The example keeps \o "1-3" \h \z \u, so its collection scope matches that of the default table of contents.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttcand the bold fontmsyhbd.ttcinto the virtual file system (VFS). - Call
buildReportto build the sample document; on the contents page, construct aTableOfContentobject with field switches, add it to a paragraph, complete the field marks and assign it toDocument.TOC. - Save the document with the
SaveToFilemethod under the file name "Create a Custom TOC in Word Document.docx". - Call the
downloadmethod to read the result file from VFS and trigger the browser download.
The code for adding a custom table of contents is as follows. It also inlines the builder for the sample document, so the whole block can replace the entire contents of App.js:
function App() {
const addCustomToc = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// Build the sample document and insert a custom TOC on the contents page
const report = buildReport(docModule, (doc, section) => {
// Construct a TOC object with custom field switches
const toc = new docModule.TableOfContent(doc, '{\\o "1-3" \\h \\z \\u}');
const tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
// Complete the field separator and field end marks
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText('TOC');
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
// Bind this TOC to the document
doc.TOC = toc;
});
// Save the result file
const outputFileName = 'Create a Custom TOC in Word Document.docx';
report.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
report.Dispose();
// Read the generated file from VFS and trigger the download
download(outputFileName);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Add a Custom Table of Contents to a New Document</h1>
<button onClick={addCustomToc}>
Generate
</button>
</div>
);
}
export default App;
The entry levels and hyperlinks of a custom table of contents are determined by the switch string, and the example collects the same range as the default table of contents:
Table of contents generated with TableOfContent and custom field switches

Modify the Table of Contents Entry Styles
To modify the entry styles of the table of contents, rewrite the three paragraph styles TOC 1 to TOC 3. After the table of contents is updated, entries at each level use the corresponding style, so restyling the styles changes the font, size and indentation of all entries at once, without setting them one by one.
| Style | Controls |
|---|---|
TOC 1 |
Entries generated from Heading 1 |
TOC 2 |
Entries generated from Heading 2 |
TOC 3 |
Entries generated from Heading 3 |
The example sets first-level entries to 11 pt and bold, with 3 pt of space before, and second- and third-level entries to 10.5 pt, with a left indent that grows by 18 pt per level.
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the body font
msyh.ttcand the bold fontmsyhbd.ttcinto the virtual file system (VFS). - Call
buildReportto build the sample document, and insert the default TOC field on the contents page withAppendTOC(1, 3). - Once the table of contents has been updated, get the TOC 1 to TOC 3 styles and set the font, size, bold, line spacing and left indent.
- Save the document with the
SaveToFilemethod, then call thedownloadmethod to read the result file from VFS and trigger the download.
The code for modifying the entry styles is as follows:
function App() {
const styleTocEntries = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Make sure the WASM module has fully loaded
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the fonts into VFS
await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// Build the sample document and insert the default TOC field on the contents page
const report = buildReport(docModule, (doc, section) => {
const tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
});
// Once the TOC has been updated, rewrite the three entry styles TOC 1 to TOC 3
for (let level = 1; level <= 3; level++) {
const style = report.Styles.get_Item('TOC ' + level);
if (!style) continue;
style.CharacterFormat.FontName = 'Microsoft YaHei';
style.CharacterFormat.FontSize = level === 1 ? 11 : 10.5;
style.CharacterFormat.Bold = level === 1;
style.ParagraphFormat.LineSpacingRule = docModule.LineSpacingRule.Multiple;
style.ParagraphFormat.LineSpacing = 16;
style.ParagraphFormat.BeforeSpacing = level === 1 ? 3 : 0;
style.ParagraphFormat.AfterSpacing = 1;
style.ParagraphFormat.LeftIndent = (level - 1) * 18;
}
// Save the result file
const outputFileName = 'Set Word TOC Entry Styles.docx';
report.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
report.Dispose();
// Read the generated file from VFS and trigger the download
download(outputFileName);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Modify the Table of Contents Entry Styles in Word</h1>
<button onClick={styleTocEntries}>
Generate
</button>
</div>
);
}
export default App;
After the styles are rewritten, first-level entries are larger and bold, and second- and third-level entries are indented by level:
Table of contents after the TOC 1 to TOC 3 styles are rewritten

FAQ
What should I do when the table of contents reports "No table of contents entries found" after updating
Cause: The table of contents collects entries by heading style or outline level. If a paragraph is only made bold or given a larger size, without a built-in heading style such as Heading 1 to Heading 3 applied, no collectable entry can be found when the field is updated.
Solution: Call ApplyStyle on the paragraphs that should appear in the table of contents to apply a built-in heading style:
const p = section.AddParagraph();
p.AppendText('Chapter 1 Project Overview');
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
The table of contents shows only the word TOC, with no entries or page numbers
Cause: AppendTOC and TableOfContent insert a TOC field, and the entries and page numbers are written when the field is updated. If UpdateTableOfContents is not called before saving, the page shows only the placeholder text.
Solution: Update the table of contents before SaveToFile:
doc.UpdateTableOfContents();
Updating a custom table of contents throws an ArgumentOutOfRange error
Cause: After a TableOfContent object is added to a paragraph, if the field separator, the field end mark, or the assignment to Document.TOC is missing, the TOC field is incomplete, and updating it may throw an index-out-of-range exception.
Solution: All three steps below have to be completed together:
const toc = new docModule.TableOfContent(doc, '{\\o "1-3" \\h \\z \\u}');
const tocPara = section.AddParagraph();
tocPara.Items.Add(toc);
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
tocPara.AppendText('TOC');
tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);
doc.TOC = toc;
The page numbers in the table of contents do not match the actual pages
Cause: The page numbers of the table of contents are written from the layout at the moment the field is updated, so they do not follow changes made to the body text or headings afterwards. In the example the cover and the table of contents each take one page, so Chapter 1 of the body starts on page 3; that is an expected result.
Solution: Call UpdateTableOfContents again before saving, so that the entries and page numbers are recalculated from the latest content:
doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
Can only the headings of some chapters be collected
Solution: The levels to collect can be limited through the start and end levels of AppendTOC; for example, AppendTOC(1, 2) collects only two heading levels. Paragraphs that should not appear in the table of contents can avoid heading styles and be formatted directly instead, and a custom table of contents can limit the collection range with the \o switch.
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.

Markdown is convenient for writing documentation, README files, notes, and other structured content. But when the content needs to be printed, archived, or shared in a fixed-layout format, PDF is often more practical.
In a React application, you can convert Markdown to PDF using Spire.Doc for JavaScript. The library runs through WebAssembly (WASM), allowing Markdown content to be processed and PDF files to be generated locally in the browser without sending files to a server.
This tutorial covers three common conversion scenarios:
- Convert a Markdown file to PDF with JavaScript
- Convert Markdown to PDF with custom page settings
- Convert a Markdown string to PDF with JavaScript
Set Up Spire.Doc for JavaScript in React
Before converting Markdown files, you need to integrate Spire.Doc for JavaScript into your React project and prepare the required WebAssembly runtime files.
For a detailed setup guide, see How to Integrate Spire.Doc for JavaScript in a React Project.
Step 1: Install the Package
Run the following command in your React project directory to install the required package from npm:
npm i spire.office
Step 2: Add the Required Runtime Files
Copy the following files and folders from node_modules/spire.office to your project's public directory:
_framework
spire.doc.js
Spire.Doc.Wasm.zip
spire.common.js
Spire.Common.Wasm.zip
The examples below also use CALIBRI.ttf for PDF text rendering. Place the font under:
public/static/font/
For file-based conversion, place the sample Markdown file under:
public/static/data/
The relevant project structure should look like this:
public/
├── _framework/
├── spire.doc.js
├── Spire.Doc.Wasm.zip
├── spire.common.js
├── Spire.Common.Wasm.zip
└── static/
├── data/
│ └── MarkdownExample.md
└── font/
└── CALIBRI.ttf
Note: The examples use process.env.PUBLIC_URL, which follows the Create React App convention. If your project uses Vite or another build tool, adjust the public asset paths accordingly.
Convert a Markdown File to PDF with JavaScript
Converting a Markdown file to PDF involves 4 main steps:
- Load the required font and Markdown file into the WASM virtual file system (VFS).
- Call
Document.LoadFromFile()withFileFormat.Markdownto load the Markdown file into aDocumentobject. - Call
Document.SaveToFile()withFileFormat.PDFto save the loaded document as a PDF file. - Read the generated PDF from the VFS and download it in the browser.
The following JavaScript example loads a .md file and saves it as a .pdf file:
import React, { useEffect, useState } from 'react';
function App() {
const [wasmModule, setWasmModule] = useState(null);
// Initialize the Spire.Doc WebAssembly module
useEffect(() => {
(async () => {
try {
const publicUrl = process.env.PUBLIC_URL || '';
const spireModule = await import(
/* webpackIgnore: true */
`${publicUrl}/spire.doc.js`
);
const rawModule = spireModule.default || spireModule;
window.wasmModule =
typeof rawModule === 'function'
? await rawModule({
locateFile: (path) =>
path.endsWith('.wasm')
? `${publicUrl}/${path}`
: path
})
: rawModule;
setWasmModule(window.wasmModule);
} catch (error) {
console.error(
'Failed to load the Spire.Doc WASM module:',
error
);
}
})();
}, []);
// Download a file generated in the WASM virtual file system
const downloadVfsFile = (fileName, mimeType) => {
const fileData =
window.dotnetRuntime.Module.FS.readFile(fileName);
const blob = new Blob(
[fileData],
{ type: mimeType }
);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const convertMarkdownToPdf = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) return;
const publicUrl = process.env.PUBLIC_URL || '';
// Load the font into the VFS
await window.spire.FetchFileToVFS(
'CALIBRI.ttf',
'/Library/Fonts/',
`${publicUrl}/static/font/`
);
const inputFileName = 'MarkdownExample.md';
const outputFileName = 'MarkdownToPDF.pdf';
// Load the Markdown file into the VFS
await window.spire.FetchFileToVFS(
inputFileName,
'',
`${publicUrl}/static/data/`
);
const doc = new docModule.Document();
try {
// Load the Markdown file
doc.LoadFromFile({
fileName: inputFileName,
fileFormat: docModule.FileFormat.Markdown
});
// Save the document as PDF
doc.SaveToFile({
fileName: outputFileName,
fileFormat: docModule.FileFormat.PDF
});
// Download the generated PDF
downloadVfsFile(
outputFileName,
'application/pdf'
);
} finally {
doc.Dispose();
}
};
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<h1>Convert Markdown to PDF</h1>
<button
onClick={convertMarkdownToPdf}
disabled={!wasmModule}
>
Convert and Download PDF
</button>
</div>
);
}
export default App;
Run the application and wait for the WASM module to finish loading. Then click Convert and Download PDF. The application will load MarkdownExample.md, convert it to MarkdownToPDF.pdf, and download the generated PDF in the browser.
Output
The generated PDF preserves the main Markdown structure, including headings, paragraphs, lists, and tables:

Convert Markdown to PDF with Custom Page Settings
The default page layout may not suit every document. A Markdown report containing a wide table, for example, may work better in landscape orientation, while printable documentation may require specific page sizes or margins.
After loading the Markdown file, you can access the document section and adjust its PageSetup properties before generating the PDF.
The following example sets the first section to A4 size, landscape orientation, and 50-point margins:
const section = doc.Sections.get_Item(0);
// Set page size
section.PageSetup.PageSize =
docModule.PageSize.A4();
// Set page orientation
section.PageSetup.Orientation =
docModule.PageOrientation.Landscape;
// Set page margins
section.PageSetup.Margins.All = 50;
To apply these settings during Markdown-to-PDF conversion, use the following function:
const convertMarkdownToPdfWithPageSettings = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) return;
const publicUrl = process.env.PUBLIC_URL || '';
// Load the font into the VFS
await window.spire.FetchFileToVFS(
'CALIBRI.ttf',
'/Library/Fonts/',
`${publicUrl}/static/font/`
);
const inputFileName = 'MarkdownExample.md';
const outputFileName =
'MarkdownToPDFWithPageSettings.pdf';
// Load the Markdown file into the VFS
await window.spire.FetchFileToVFS(
inputFileName,
'',
`${publicUrl}/static/data/`
);
const doc = new docModule.Document();
try {
// Load the Markdown file
doc.LoadFromFile({
fileName: inputFileName,
fileFormat: docModule.FileFormat.Markdown
});
// Get the first section
const section = doc.Sections.get_Item(0);
// Set page size
section.PageSetup.PageSize =
docModule.PageSize.A4();
// Set landscape orientation
section.PageSetup.Orientation =
docModule.PageOrientation.Landscape;
// Set all margins to 50 points
section.PageSetup.Margins.All = 50;
// Save the document as PDF
doc.SaveToFile({
fileName: outputFileName,
fileFormat: docModule.FileFormat.PDF
});
// Download the generated PDF
downloadVfsFile(
outputFileName,
'application/pdf'
);
} finally {
doc.Dispose();
}
};
You can adjust the page size, orientation, and margins according to the content of your Markdown document.
Convert a Markdown String to PDF with JavaScript
Markdown does not always come from a physical .md file. In a React application, the content may already exist as a string from a Markdown editor, textarea, CMS, API response, or application state.
In this case, write the Markdown string to the WASM virtual file system using FS.writeFile(), then load the virtual .md file and convert it to PDF.
const convertMarkdownStringToPdf = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) return;
const publicUrl = process.env.PUBLIC_URL || '';
// Load the font into the VFS
await window.spire.FetchFileToVFS(
'CALIBRI.ttf',
'/Library/Fonts/',
`${publicUrl}/static/font/`
);
const markdownString = `# Project Notes
This PDF was generated from **Markdown stored in a React string**.
## Tasks
- Review the draft
- Export the final copy
- Share the PDF
## Task Status
| Item | Status |
| --- | --- |
| Draft | Done |
| Review | Pending |
`;
const inputFileName = 'MarkdownInput.md';
const outputFileName = 'MarkdownStringToPDF.pdf';
// Write the Markdown string to the VFS
window.dotnetRuntime.Module.FS.writeFile(
inputFileName,
markdownString,
{ encoding: 'utf8' }
);
const doc = new docModule.Document();
try {
// Load the virtual Markdown file
doc.LoadFromFile({
fileName: inputFileName,
fileFormat: docModule.FileFormat.Markdown
});
// Save the document as PDF
doc.SaveToFile({
fileName: outputFileName,
fileFormat: docModule.FileFormat.PDF
});
// Download the generated PDF
downloadVfsFile(
outputFileName,
'application/pdf'
);
} finally {
doc.Dispose();
}
};
In an actual application, replace the sample string with the Markdown content from your existing data source:
const markdownString = editorValue;
or:
const markdownString = apiResponse.content;
The rest of the PDF conversion workflow remains the same.
Output

Why Are Fonts Loaded into the VFS?
PDF generation requires font data to render text correctly. In the examples above, CALIBRI.ttf is loaded into the WASM virtual file system before the Markdown document is processed:
await window.spire.FetchFileToVFS(
'CALIBRI.ttf',
'/Library/Fonts/',
`${publicUrl}/static/font/`
);
If the Markdown contains characters that are not supported by the selected font, load an appropriate font into /Library/Fonts/ as well.
This is particularly important when generating PDFs containing Chinese, Japanese, Korean, Arabic, or other multilingual text.
FAQs
Can I Convert a User-Uploaded Markdown File to PDF?
Yes. Instead of loading a fixed .md file from the public directory, you can read the uploaded Markdown file in the browser, write its content to the WASM virtual file system, and then load it with FileFormat.Markdown. This allows users to select and convert their own Markdown files directly in a React application.
Why Are Some Characters Missing from the Generated PDF?
This usually happens when the font required to display those characters is not available in the WASM environment. Load a font that supports the characters used in your Markdown into /Library/Fonts/ before generating the PDF. This is especially important for multilingual content.
What Should I Do If a Wide Markdown Table Is Cut Off?
Try switching the page to landscape orientation, reducing the margins, or using a larger page size before saving the document as PDF.
Why Are Images in My Markdown Missing from the PDF?
Markdown usually references images through a file path or URL rather than embedding the image data directly. Make sure the image files referenced in the Markdown are available during conversion and that their paths can be resolved by the conversion environment. Relative image paths may require additional handling depending on where the Markdown and image files are stored.
Does the Markdown-to-PDF Conversion Run Locally in the Browser?
Yes. In this React implementation, Spire.Doc for JavaScript runs through WebAssembly, while the Markdown input and generated PDF are processed through the browser-side virtual file system. A backend is not required for the conversion itself, although your application may still use one for file storage, authentication, or other server-side operations.
Conclusion
This article demonstrated how to convert Markdown to PDF with JavaScript in a React application, including basic file conversion, custom page settings, and conversion from Markdown strings.
With Spire.Doc for JavaScript, developers can load Markdown content, control PDF page layout, and generate PDF files directly in the browser through WebAssembly. This approach can be used for documentation tools, Markdown editors, reporting systems, and other applications that need to export Markdown content as PDF.
Get a Free License
To fully experience the capabilities of Spire.Doc for JavaScript without any evaluation limitations, you can request a free 30-day trial license.
More...